dalli 2.7.5 → 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,10 +1,11 @@
1
- require 'digest/md5'
2
- require 'set'
1
+ # frozen_string_literal: true
2
+
3
+ require "digest/md5"
4
+ require "set"
3
5
 
4
6
  # encoding: ascii
5
7
  module Dalli
6
8
  class Client
7
-
8
9
  ##
9
10
  # Dalli::Client is the main class which developers will use to interact with
10
11
  # the memcached server. Usage:
@@ -28,9 +29,12 @@ module Dalli
28
29
  # - :serializer - defaults to Marshal
29
30
  # - :compressor - defaults to zlib
30
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.
31
34
  #
32
- def initialize(servers=nil, options={})
33
- @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")
34
38
  @options = normalize_options(options)
35
39
  @ring = nil
36
40
  end
@@ -54,7 +58,7 @@ module Dalli
54
58
  ##
55
59
  # Get the value associated with the key.
56
60
  # If a value is not found, then +nil+ is returned.
57
- def get(key, options=nil)
61
+ def get(key, options = nil)
58
62
  perform(:get, key, options)
59
63
  end
60
64
 
@@ -63,12 +67,15 @@ module Dalli
63
67
  # If a block is given, yields key/value pairs one at a time.
64
68
  # Otherwise returns a hash of { 'key' => 'value', 'key2' => 'value1' }
65
69
  def get_multi(*keys)
66
- return {} if keys.flatten.compact.empty?
70
+ keys.flatten!
71
+ keys.compact!
72
+
73
+ return {} if keys.empty?
67
74
  if block_given?
68
- get_multi_yielder(keys) {|k, data| yield k, data.first}
75
+ get_multi_yielder(keys) { |k, data| yield k, data.first }
69
76
  else
70
- Hash.new.tap do |hash|
71
- 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 }
72
79
  end
73
80
  end
74
81
  end
@@ -83,11 +90,11 @@ module Dalli
83
90
  # If a value is not found (or if the found value is nil and :cache_nils is false)
84
91
  # and a block is given, the block will be invoked and its return value
85
92
  # written to the cache and returned.
86
- def fetch(key, ttl=nil, options=nil)
93
+ def fetch(key, ttl = nil, options = nil)
87
94
  options = options.nil? ? CACHE_NILS : options.merge(CACHE_NILS) if @options[:cache_nils]
88
95
  val = get(key, options)
89
96
  not_found = @options[:cache_nils] ?
90
- val == Dalli::Server::NOT_FOUND :
97
+ val == Dalli::Protocol::NOT_FOUND :
91
98
  val.nil?
92
99
  if not_found && block_given?
93
100
  val = yield
@@ -107,30 +114,36 @@ module Dalli
107
114
  # - nil if the key did not exist.
108
115
  # - false if the value was changed by someone else.
109
116
  # - true if the value was successfully updated.
110
- def cas(key, ttl=nil, options=nil)
111
- (value, cas) = perform(:cas, key)
112
- value = (!value || value == 'Not found') ? nil : value
113
- if value
114
- newvalue = yield(value)
115
- perform(:set, key, newvalue, ttl_or_default(ttl), cas, options)
116
- end
117
+ def cas(key, ttl = nil, options = nil, &block)
118
+ cas_core(key, false, ttl, options, &block)
119
+ end
120
+
121
+ ##
122
+ # like #cas, but will yield to the block whether or not the value
123
+ # already exists.
124
+ #
125
+ # Returns:
126
+ # - false if the value was changed by someone else.
127
+ # - true if the value was successfully updated.
128
+ def cas!(key, ttl = nil, options = nil, &block)
129
+ cas_core(key, true, ttl, options, &block)
117
130
  end
118
131
 
119
- def set(key, value, ttl=nil, options=nil)
132
+ def set(key, value, ttl = nil, options = nil)
120
133
  perform(:set, key, value, ttl_or_default(ttl), 0, options)
121
134
  end
122
135
 
123
136
  ##
124
137
  # Conditionally add a key/value pair, if the key does not already exist
125
138
  # on the server. Returns truthy if the operation succeeded.
126
- def add(key, value, ttl=nil, options=nil)
139
+ def add(key, value, ttl = nil, options = nil)
127
140
  perform(:add, key, value, ttl_or_default(ttl), options)
128
141
  end
129
142
 
130
143
  ##
131
144
  # Conditionally add a key/value pair, only if the key already exists
132
145
  # on the server. Returns truthy if the operation succeeded.
133
- def replace(key, value, ttl=nil, options=nil)
146
+ def replace(key, value, ttl = nil, options = nil)
134
147
  perform(:replace, key, value, ttl_or_default(ttl), 0, options)
135
148
  end
136
149
 
@@ -152,7 +165,7 @@ module Dalli
152
165
  perform(:prepend, key, value.to_s)
153
166
  end
154
167
 
155
- def flush(delay=0)
168
+ def flush(delay = 0)
156
169
  time = -delay
157
170
  ring.servers.map { |s| s.request(:flush, time += delay) }
158
171
  end
@@ -170,7 +183,7 @@ module Dalli
170
183
  # Note that the ttl will only apply if the counter does not already
171
184
  # exist. To increase an existing counter and update its TTL, use
172
185
  # #cas.
173
- def incr(key, amt=1, ttl=nil, default=nil)
186
+ def incr(key, amt = 1, ttl = nil, default = nil)
174
187
  raise ArgumentError, "Positive values only: #{amt}" if amt < 0
175
188
  perform(:incr, key, amt.to_i, ttl_or_default(ttl), default)
176
189
  end
@@ -189,7 +202,7 @@ module Dalli
189
202
  # Note that the ttl will only apply if the counter does not already
190
203
  # exist. To decrease an existing counter and update its TTL, use
191
204
  # #cas.
192
- def decr(key, amt=1, ttl=nil, default=nil)
205
+ def decr(key, amt = 1, ttl = nil, default = nil)
193
206
  raise ArgumentError, "Positive values only: #{amt}" if amt < 0
194
207
  perform(:decr, key, amt.to_i, ttl_or_default(ttl), default)
195
208
  end
@@ -198,20 +211,28 @@ module Dalli
198
211
  # Touch updates expiration time for a given key.
199
212
  #
200
213
  # Returns true if key exists, otherwise nil.
201
- def touch(key, ttl=nil)
214
+ def touch(key, ttl = nil)
202
215
  resp = perform(:touch, key, ttl_or_default(ttl))
203
216
  resp.nil? ? nil : true
204
217
  end
205
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
+
206
227
  ##
207
228
  # Collect the stats for each server.
208
229
  # You can optionally pass a type including :items, :slabs or :settings to get specific stats
209
230
  # Returns a hash like { 'hostname:port' => { 'stat1' => 'value1', ... }, 'hostname2:port' => { ... } }
210
- def stats(type=nil)
211
- type = nil if ![nil, :items,:slabs,:settings].include? type
231
+ def stats(type = nil)
232
+ type = nil unless [nil, :items, :slabs, :settings].include? type
212
233
  values = {}
213
234
  ring.servers.each do |server|
214
- 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
215
236
  end
216
237
  values
217
238
  end
@@ -235,11 +256,63 @@ module Dalli
235
256
  def version
236
257
  values = {}
237
258
  ring.servers.each do |server|
238
- values["#{server.name}"] = server.alive? ? server.request(:version) : nil
259
+ values[server.name.to_s] = server.alive? ? server.request(:version) : nil
239
260
  end
240
261
  values
241
262
  end
242
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
+
243
316
  ##
244
317
  # Close our connection to each server.
245
318
  # If you perform another operation after this, the connections will be re-established.
@@ -258,6 +331,14 @@ module Dalli
258
331
 
259
332
  private
260
333
 
334
+ def cas_core(key, always_set, ttl = nil, options = nil)
335
+ (value, cas) = perform(:cas, key)
336
+ value = !value || value == "Not found" ? nil : value
337
+ return if value.nil? && !always_set
338
+ newvalue = yield(value)
339
+ perform(:set, key, newvalue, ttl_or_default(ttl), cas, options)
340
+ end
341
+
261
342
  def ttl_or_default(ttl)
262
343
  (ttl || @options[:expires_in]).to_i
263
344
  rescue NoMethodError
@@ -265,33 +346,29 @@ module Dalli
265
346
  end
266
347
 
267
348
  def groups_for_keys(*keys)
268
- 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|
269
353
  begin
270
354
  ring.server_for_key(key)
271
355
  rescue Dalli::RingError
272
356
  Dalli.logger.debug { "unable to get key #{key}" }
273
357
  nil
274
358
  end
275
- end
276
- return groups
277
- end
278
-
279
- def mapped_keys(keys)
280
- keys.flatten.map {|a| validate_key(a.to_s)}
359
+ }
281
360
  end
282
361
 
283
362
  def make_multi_get_requests(groups)
284
363
  groups.each do |server, keys_for_server|
285
- begin
286
- # TODO: do this with the perform chokepoint?
287
- # But given the fact that fetching the response doesn't take place
288
- # in that slot it's misleading anyway. Need to move all of this method
289
- # into perform to be meaningful
290
- server.request(:send_multiget, keys_for_server)
291
- rescue DalliError, NetworkError => e
292
- Dalli.logger.debug { e.inspect }
293
- Dalli.logger.debug { "unable to get keys for server #{server.name}" }
294
- 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}" }
295
372
  end
296
373
  end
297
374
 
@@ -309,44 +386,55 @@ module Dalli
309
386
  servers
310
387
  end
311
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
+
312
399
  ##
313
400
  # Normalizes the argument into an array of servers.
314
- # 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.
315
402
  # "memcache1.example.com:11211,memcache2.example.com:11211,memcache3.example.com:11211"
316
403
  def normalize_servers(servers)
317
- if servers.is_a? String
318
- return servers.split(",")
319
- else
320
- return servers
404
+ Array(servers).flat_map do |server|
405
+ if server.is_a? String
406
+ server.split(",")
407
+ else
408
+ server
409
+ end
321
410
  end
322
411
  end
323
412
 
324
413
  def ring
325
414
  @ring ||= Dalli::Ring.new(
326
- @servers.map do |s|
327
- server_options = {}
328
- if s =~ %r{\Amemcached://}
415
+ @servers.map { |s|
416
+ server_options = {}
417
+ if s.start_with?("memcached://")
329
418
  uri = URI.parse(s)
330
419
  server_options[:username] = uri.user
331
420
  server_options[:password] = uri.password
332
421
  s = "#{uri.host}:#{uri.port}"
333
422
  end
334
- Dalli::Server.new(s, @options.merge(server_options))
335
- end, @options
423
+ @options.fetch(:protocol_implementation, Dalli::Protocol::Binary).new(s, @options.merge(server_options))
424
+ }, @options
336
425
  )
337
426
  end
338
427
 
339
428
  # Chokepoint method for instrumentation
340
- def perform(*all_args, &blk)
341
- return blk.call if blk
342
- op, key, *args = *all_args
429
+ def perform(*all_args)
430
+ return yield if block_given?
431
+ op, key, *args = all_args
343
432
 
344
433
  key = key.to_s
345
434
  key = validate_key(key)
346
435
  begin
347
436
  server = ring.server_for_key(key)
348
- ret = server.request(op, key, *args)
349
- ret
437
+ server.request(op, key, *args)
350
438
  rescue NetworkError => e
351
439
  Dalli.logger.debug { e.inspect }
352
440
  Dalli.logger.debug { "retrying request with new server" }
@@ -358,10 +446,11 @@ module Dalli
358
446
  raise ArgumentError, "key cannot be blank" if !key || key.length == 0
359
447
  key = key_with_namespace(key)
360
448
  if key.length > 250
361
- max_length_before_namespace = 212 - (namespace || '').size
362
- 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)}"
363
452
  end
364
- return key
453
+ key
365
454
  end
366
455
 
367
456
  def key_with_namespace(key)
@@ -369,7 +458,7 @@ module Dalli
369
458
  end
370
459
 
371
460
  def key_without_namespace(key)
372
- (ns = namespace) ? key.sub(%r(\A#{Regexp.escape ns}:), '') : key
461
+ (ns = namespace) ? key.sub(%r{\A#{Regexp.escape ns}:}, "") : key
373
462
  end
374
463
 
375
464
  def namespace
@@ -387,6 +476,9 @@ module Dalli
387
476
  rescue NoMethodError
388
477
  raise ArgumentError, "cannot convert :expires_in => #{opts[:expires_in].inspect} to an integer"
389
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
390
482
  opts
391
483
  end
392
484
 
@@ -396,54 +488,52 @@ module Dalli
396
488
  perform do
397
489
  return {} if keys.empty?
398
490
  ring.lock do
399
- begin
400
- groups = groups_for_keys(keys)
401
- if unfound_keys = groups.delete(nil)
402
- Dalli.logger.debug { "unable to get keys for #{unfound_keys.length} keys because no matching server was found" }
403
- end
404
- make_multi_get_requests(groups)
405
-
406
- servers = groups.keys
407
- return if servers.empty?
408
- servers = perform_multi_response_start(servers)
409
-
410
- start = Time.now
411
- loop do
412
- # remove any dead servers
413
- servers.delete_if { |s| s.sock.nil? }
414
- break if servers.empty?
415
-
416
- # calculate remaining timeout
417
- elapsed = Time.now - start
418
- timeout = servers.first.options[:socket_timeout]
419
- time_left = (elapsed > timeout) ? 0 : timeout - elapsed
420
-
421
- sockets = servers.map(&:sock)
422
- readable, _ = IO.select(sockets, nil, nil, time_left)
423
-
424
- if readable.nil?
425
- # no response within timeout; abort pending connections
426
- servers.each do |server|
427
- Dalli.logger.debug { "memcached at #{server.name} did not response within timeout" }
428
- server.multi_response_abort
429
- end
430
- 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
431
522
 
432
- else
433
- readable.each do |sock|
434
- server = sock.server
523
+ else
524
+ readable.each do |sock|
525
+ server = sock.server
435
526
 
436
- begin
437
- server.multi_response_nonblock.each_pair do |key, value_list|
438
- yield key_without_namespace(key), value_list
439
- end
527
+ begin
528
+ server.multi_response_nonblock.each_pair do |key, value_list|
529
+ yield key_without_namespace(key), value_list
530
+ end
440
531
 
441
- if server.multi_response_completed?
442
- servers.delete(server)
443
- end
444
- rescue NetworkError
532
+ if server.multi_response_completed?
445
533
  servers.delete(server)
446
534
  end
535
+ rescue NetworkError
536
+ servers.delete(server)
447
537
  end
448
538
  end
449
539
  end
@@ -1,5 +1,7 @@
1
- require 'zlib'
2
- require 'stringio'
1
+ # frozen_string_literal: true
2
+
3
+ require "zlib"
4
+ require "stringio"
3
5
 
4
6
  module Dalli
5
7
  class Compressor
@@ -14,7 +16,7 @@ module Dalli
14
16
 
15
17
  class GzipCompressor
16
18
  def self.compress(data)
17
- io = StringIO.new("w")
19
+ io = StringIO.new(+"", "w")
18
20
  gz = Zlib::GzipWriter.new(io)
19
21
  gz.write(data)
20
22
  gz.close
data/lib/dalli/options.rb CHANGED
@@ -1,12 +1,12 @@
1
- require 'thread'
2
- require 'monitor'
1
+ # frozen_string_literal: true
3
2
 
4
- module Dalli
3
+ require "monitor"
5
4
 
5
+ module Dalli
6
6
  # Make Dalli threadsafe by using a lock around all
7
7
  # public server methods.
8
8
  #
9
- # Dalli::Server.extend(Dalli::Threadsafe)
9
+ # Dalli::Protocol::Binary.extend(Dalli::Threadsafe)
10
10
  #
11
11
  module Threadsafe
12
12
  def self.extended(obj)