dalli 2.7.9 → 3.0.1

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,15 +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
- check_keys = keys.flatten
68
- check_keys.compact!
70
+ keys.flatten!
71
+ keys.compact!
69
72
 
70
- return {} if check_keys.empty?
73
+ return {} if keys.empty?
71
74
  if block_given?
72
- get_multi_yielder(keys) {|k, data| yield k, data.first}
75
+ get_multi_yielder(keys) { |k, data| yield k, data.first }
73
76
  else
74
- Hash.new.tap do |hash|
75
- 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 }
76
79
  end
77
80
  end
78
81
  end
@@ -87,11 +90,11 @@ module Dalli
87
90
  # If a value is not found (or if the found value is nil and :cache_nils is false)
88
91
  # and a block is given, the block will be invoked and its return value
89
92
  # written to the cache and returned.
90
- def fetch(key, ttl=nil, options=nil)
93
+ def fetch(key, ttl = nil, options = nil)
91
94
  options = options.nil? ? CACHE_NILS : options.merge(CACHE_NILS) if @options[:cache_nils]
92
95
  val = get(key, options)
93
96
  not_found = @options[:cache_nils] ?
94
- val == Dalli::Server::NOT_FOUND :
97
+ val == Dalli::Protocol::NOT_FOUND :
95
98
  val.nil?
96
99
  if not_found && block_given?
97
100
  val = yield
@@ -111,7 +114,7 @@ module Dalli
111
114
  # - nil if the key did not exist.
112
115
  # - false if the value was changed by someone else.
113
116
  # - true if the value was successfully updated.
114
- def cas(key, ttl=nil, options=nil, &block)
117
+ def cas(key, ttl = nil, options = nil, &block)
115
118
  cas_core(key, false, ttl, options, &block)
116
119
  end
117
120
 
@@ -122,25 +125,25 @@ module Dalli
122
125
  # Returns:
123
126
  # - false if the value was changed by someone else.
124
127
  # - true if the value was successfully updated.
125
- def cas!(key, ttl=nil, options=nil, &block)
128
+ def cas!(key, ttl = nil, options = nil, &block)
126
129
  cas_core(key, true, ttl, options, &block)
127
130
  end
128
131
 
129
- def set(key, value, ttl=nil, options=nil)
132
+ def set(key, value, ttl = nil, options = nil)
130
133
  perform(:set, key, value, ttl_or_default(ttl), 0, options)
131
134
  end
132
135
 
133
136
  ##
134
137
  # Conditionally add a key/value pair, if the key does not already exist
135
138
  # on the server. Returns truthy if the operation succeeded.
136
- def add(key, value, ttl=nil, options=nil)
139
+ def add(key, value, ttl = nil, options = nil)
137
140
  perform(:add, key, value, ttl_or_default(ttl), options)
138
141
  end
139
142
 
140
143
  ##
141
144
  # Conditionally add a key/value pair, only if the key already exists
142
145
  # on the server. Returns truthy if the operation succeeded.
143
- def replace(key, value, ttl=nil, options=nil)
146
+ def replace(key, value, ttl = nil, options = nil)
144
147
  perform(:replace, key, value, ttl_or_default(ttl), 0, options)
145
148
  end
146
149
 
@@ -162,7 +165,7 @@ module Dalli
162
165
  perform(:prepend, key, value.to_s)
163
166
  end
164
167
 
165
- def flush(delay=0)
168
+ def flush(delay = 0)
166
169
  time = -delay
167
170
  ring.servers.map { |s| s.request(:flush, time += delay) }
168
171
  end
@@ -180,7 +183,7 @@ module Dalli
180
183
  # Note that the ttl will only apply if the counter does not already
181
184
  # exist. To increase an existing counter and update its TTL, use
182
185
  # #cas.
183
- def incr(key, amt=1, ttl=nil, default=nil)
186
+ def incr(key, amt = 1, ttl = nil, default = nil)
184
187
  raise ArgumentError, "Positive values only: #{amt}" if amt < 0
185
188
  perform(:incr, key, amt.to_i, ttl_or_default(ttl), default)
186
189
  end
@@ -199,7 +202,7 @@ module Dalli
199
202
  # Note that the ttl will only apply if the counter does not already
200
203
  # exist. To decrease an existing counter and update its TTL, use
201
204
  # #cas.
202
- def decr(key, amt=1, ttl=nil, default=nil)
205
+ def decr(key, amt = 1, ttl = nil, default = nil)
203
206
  raise ArgumentError, "Positive values only: #{amt}" if amt < 0
204
207
  perform(:decr, key, amt.to_i, ttl_or_default(ttl), default)
205
208
  end
@@ -208,20 +211,28 @@ module Dalli
208
211
  # Touch updates expiration time for a given key.
209
212
  #
210
213
  # Returns true if key exists, otherwise nil.
211
- def touch(key, ttl=nil)
214
+ def touch(key, ttl = nil)
212
215
  resp = perform(:touch, key, ttl_or_default(ttl))
213
216
  resp.nil? ? nil : true
214
217
  end
215
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
+
216
227
  ##
217
228
  # Collect the stats for each server.
218
229
  # You can optionally pass a type including :items, :slabs or :settings to get specific stats
219
230
  # Returns a hash like { 'hostname:port' => { 'stat1' => 'value1', ... }, 'hostname2:port' => { ... } }
220
- def stats(type=nil)
221
- type = nil if ![nil, :items,:slabs,:settings].include? type
231
+ def stats(type = nil)
232
+ type = nil unless [nil, :items, :slabs, :settings].include? type
222
233
  values = {}
223
234
  ring.servers.each do |server|
224
- 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
225
236
  end
226
237
  values
227
238
  end
@@ -245,11 +256,63 @@ module Dalli
245
256
  def version
246
257
  values = {}
247
258
  ring.servers.each do |server|
248
- values["#{server.name}"] = server.alive? ? server.request(:version) : nil
259
+ values[server.name.to_s] = server.alive? ? server.request(:version) : nil
249
260
  end
250
261
  values
251
262
  end
252
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
+
253
316
  ##
254
317
  # Close our connection to each server.
255
318
  # If you perform another operation after this, the connections will be re-established.
@@ -268,9 +331,9 @@ module Dalli
268
331
 
269
332
  private
270
333
 
271
- def cas_core(key, always_set, ttl=nil, options=nil)
334
+ def cas_core(key, always_set, ttl = nil, options = nil)
272
335
  (value, cas) = perform(:cas, key)
273
- value = (!value || value == 'Not found') ? nil : value
336
+ value = !value || value == "Not found" ? nil : value
274
337
  return if value.nil? && !always_set
275
338
  newvalue = yield(value)
276
339
  perform(:set, key, newvalue, ttl_or_default(ttl), cas, options)
@@ -283,35 +346,29 @@ module Dalli
283
346
  end
284
347
 
285
348
  def groups_for_keys(*keys)
286
- groups = mapped_keys(keys).flatten.group_by do |key|
349
+ keys.flatten!
350
+ keys.map! { |a| validate_key(a.to_s) }
351
+
352
+ keys.group_by { |key|
287
353
  begin
288
354
  ring.server_for_key(key)
289
355
  rescue Dalli::RingError
290
356
  Dalli.logger.debug { "unable to get key #{key}" }
291
357
  nil
292
358
  end
293
- end
294
- return groups
295
- end
296
-
297
- def mapped_keys(keys)
298
- keys_array = keys.flatten
299
- keys_array.map! { |a| validate_key(a.to_s) }
300
- keys_array
359
+ }
301
360
  end
302
361
 
303
362
  def make_multi_get_requests(groups)
304
363
  groups.each do |server, keys_for_server|
305
- begin
306
- # TODO: do this with the perform chokepoint?
307
- # But given the fact that fetching the response doesn't take place
308
- # in that slot it's misleading anyway. Need to move all of this method
309
- # into perform to be meaningful
310
- server.request(:send_multiget, keys_for_server)
311
- rescue DalliError, NetworkError => e
312
- Dalli.logger.debug { e.inspect }
313
- Dalli.logger.debug { "unable to get keys for server #{server.name}" }
314
- end
364
+ # TODO: do this with the perform chokepoint?
365
+ # But given the fact that fetching the response doesn't take place
366
+ # in that slot it's misleading anyway. Need to move all of this method
367
+ # into perform to be meaningful
368
+ server.request(:send_multiget, keys_for_server)
369
+ rescue DalliError, NetworkError => e
370
+ Dalli.logger.debug { e.inspect }
371
+ Dalli.logger.debug { "unable to get keys for server #{server.name}" }
315
372
  end
316
373
  end
317
374
 
@@ -329,44 +386,55 @@ module Dalli
329
386
  servers
330
387
  end
331
388
 
389
+ ##
390
+ # Ensures that the servers arg is either an array or a string.
391
+ def validate_servers_arg(servers)
392
+ return if servers.nil?
393
+ return if servers.is_a?(Array)
394
+ return if servers.is_a?(String)
395
+
396
+ raise ArgumentError, "An explicit servers argument must be a comma separated string or an array containing strings."
397
+ end
398
+
332
399
  ##
333
400
  # Normalizes the argument into an array of servers.
334
- # If the argument is a string, it's expected that the URIs are comma separated e.g.
401
+ # If the argument is a string, or an array containing strings, it's expected that the URIs are comma separated e.g.
335
402
  # "memcache1.example.com:11211,memcache2.example.com:11211,memcache3.example.com:11211"
336
403
  def normalize_servers(servers)
337
- if servers.is_a? String
338
- return servers.split(",")
339
- else
340
- return servers
404
+ Array(servers).flat_map do |server|
405
+ if server.is_a? String
406
+ server.split(",")
407
+ else
408
+ server
409
+ end
341
410
  end
342
411
  end
343
412
 
344
413
  def ring
345
414
  @ring ||= Dalli::Ring.new(
346
- @servers.map do |s|
347
- server_options = {}
348
- if s =~ %r{\Amemcached://}
415
+ @servers.map { |s|
416
+ server_options = {}
417
+ if s.start_with?("memcached://")
349
418
  uri = URI.parse(s)
350
419
  server_options[:username] = uri.user
351
420
  server_options[:password] = uri.password
352
421
  s = "#{uri.host}:#{uri.port}"
353
422
  end
354
- Dalli::Server.new(s, @options.merge(server_options))
355
- end, @options
423
+ @options.fetch(:protocol_implementation, Dalli::Protocol::Binary).new(s, @options.merge(server_options))
424
+ }, @options
356
425
  )
357
426
  end
358
427
 
359
428
  # Chokepoint method for instrumentation
360
429
  def perform(*all_args)
361
430
  return yield if block_given?
362
- op, key, *args = *all_args
431
+ op, key, *args = all_args
363
432
 
364
433
  key = key.to_s
365
434
  key = validate_key(key)
366
435
  begin
367
436
  server = ring.server_for_key(key)
368
- ret = server.request(op, key, *args)
369
- ret
437
+ server.request(op, key, *args)
370
438
  rescue NetworkError => e
371
439
  Dalli.logger.debug { e.inspect }
372
440
  Dalli.logger.debug { "retrying request with new server" }
@@ -378,10 +446,11 @@ module Dalli
378
446
  raise ArgumentError, "key cannot be blank" if !key || key.length == 0
379
447
  key = key_with_namespace(key)
380
448
  if key.length > 250
381
- max_length_before_namespace = 212 - (namespace || '').size
382
- key = "#{key[0, max_length_before_namespace]}:md5:#{Digest::MD5.hexdigest(key)}"
449
+ digest_class = @options[:digest_class] || ::Digest::MD5
450
+ max_length_before_namespace = 212 - (namespace || "").size
451
+ key = "#{key[0, max_length_before_namespace]}:md5:#{digest_class.hexdigest(key)}"
383
452
  end
384
- return key
453
+ key
385
454
  end
386
455
 
387
456
  def key_with_namespace(key)
@@ -389,7 +458,7 @@ module Dalli
389
458
  end
390
459
 
391
460
  def key_without_namespace(key)
392
- (ns = namespace) ? key.sub(%r(\A#{Regexp.escape ns}:), '') : key
461
+ (ns = namespace) ? key.sub(%r{\A#{Regexp.escape ns}:}, "") : key
393
462
  end
394
463
 
395
464
  def namespace
@@ -407,6 +476,9 @@ module Dalli
407
476
  rescue NoMethodError
408
477
  raise ArgumentError, "cannot convert :expires_in => #{opts[:expires_in].inspect} to an integer"
409
478
  end
479
+ if opts[:digest_class] && !opts[:digest_class].respond_to?(:hexdigest)
480
+ raise ArgumentError, "The digest_class object must respond to the hexdigest method"
481
+ end
410
482
  opts
411
483
  end
412
484
 
@@ -416,54 +488,52 @@ module Dalli
416
488
  perform do
417
489
  return {} if keys.empty?
418
490
  ring.lock do
419
- begin
420
- groups = groups_for_keys(keys)
421
- if unfound_keys = groups.delete(nil)
422
- Dalli.logger.debug { "unable to get keys for #{unfound_keys.length} keys because no matching server was found" }
423
- end
424
- make_multi_get_requests(groups)
425
-
426
- servers = groups.keys
427
- return if servers.empty?
428
- servers = perform_multi_response_start(servers)
429
-
430
- start = Time.now
431
- while true
432
- # remove any dead servers
433
- servers.delete_if { |s| s.sock.nil? }
434
- break if servers.empty?
435
-
436
- # calculate remaining timeout
437
- elapsed = Time.now - start
438
- timeout = servers.first.options[:socket_timeout]
439
- time_left = (elapsed > timeout) ? 0 : timeout - elapsed
440
-
441
- sockets = servers.map(&:sock)
442
- readable, _ = IO.select(sockets, nil, nil, time_left)
443
-
444
- if readable.nil?
445
- # no response within timeout; abort pending connections
446
- servers.each do |server|
447
- Dalli.logger.debug { "memcached at #{server.name} did not response within timeout" }
448
- server.multi_response_abort
449
- end
450
- break
491
+ groups = groups_for_keys(keys)
492
+ if (unfound_keys = groups.delete(nil))
493
+ Dalli.logger.debug { "unable to get keys for #{unfound_keys.length} keys because no matching server was found" }
494
+ end
495
+ make_multi_get_requests(groups)
496
+
497
+ servers = groups.keys
498
+ return if servers.empty?
499
+ servers = perform_multi_response_start(servers)
500
+
501
+ start = Time.now
502
+ loop do
503
+ # remove any dead servers
504
+ servers.delete_if { |s| s.sock.nil? }
505
+ break if servers.empty?
506
+
507
+ # calculate remaining timeout
508
+ elapsed = Time.now - start
509
+ timeout = servers.first.options[:socket_timeout]
510
+ time_left = elapsed > timeout ? 0 : timeout - elapsed
511
+
512
+ sockets = servers.map(&:sock)
513
+ readable, _ = IO.select(sockets, nil, nil, time_left)
514
+
515
+ if readable.nil?
516
+ # no response within timeout; abort pending connections
517
+ servers.each do |server|
518
+ Dalli.logger.debug { "memcached at #{server.name} did not response within timeout" }
519
+ server.multi_response_abort
520
+ end
521
+ break
451
522
 
452
- else
453
- readable.each do |sock|
454
- server = sock.server
523
+ else
524
+ readable.each do |sock|
525
+ server = sock.server
455
526
 
456
- begin
457
- server.multi_response_nonblock.each_pair do |key, value_list|
458
- yield key_without_namespace(key), value_list
459
- end
527
+ begin
528
+ server.multi_response_nonblock.each_pair do |key, value_list|
529
+ yield key_without_namespace(key), value_list
530
+ end
460
531
 
461
- if server.multi_response_completed?
462
- servers.delete(server)
463
- end
464
- rescue NetworkError
532
+ if server.multi_response_completed?
465
533
  servers.delete(server)
466
534
  end
535
+ rescue NetworkError
536
+ servers.delete(server)
467
537
  end
468
538
  end
469
539
  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)