dalli 2.7.11 → 3.0.4

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:
@@ -25,14 +25,17 @@ module Dalli
25
25
  # - :failover - if a server is down, look for and store values on another server in the ring. Default: true.
26
26
  # - :threadsafe - ensure that only one thread is actively using a socket at a time. Default: true.
27
27
  # - :expires_in - default TTL in seconds if you do not pass TTL as a parameter to an individual operation, defaults to 0 or forever
28
- # - :compress - defaults to false, if true Dalli will compress values larger than 1024 bytes before sending them to memcached.
28
+ # - :compress - if true Dalli will compress values larger than compression_min_size bytes before sending them to memcached. Default: true.
29
+ # - :compression_min_size - the minimum size (in bytes) for which Dalli will compress values sent to Memcached. Defaults to 4K.
29
30
  # - :serializer - defaults to Marshal
30
31
  # - :compressor - defaults to zlib
31
32
  # - :cache_nils - defaults to false, if true Dalli will not treat cached nil values as 'not found' for #fetch operations.
32
33
  # - :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.
34
+ # - :protocol_implementation - defaults to Dalli::Protocol::Binary which uses the binary protocol. Allows you to pass an alternative implementation using another protocol.
33
35
  #
34
- def initialize(servers=nil, options={})
35
- @servers = normalize_servers(servers || ENV["MEMCACHE_SERVERS"] || '127.0.0.1:11211')
36
+ def initialize(servers = nil, options = {})
37
+ validate_servers_arg(servers)
38
+ @servers = normalize_servers(servers || ENV["MEMCACHE_SERVERS"] || "127.0.0.1:11211")
36
39
  @options = normalize_options(options)
37
40
  @ring = nil
38
41
  end
@@ -56,7 +59,7 @@ module Dalli
56
59
  ##
57
60
  # Get the value associated with the key.
58
61
  # If a value is not found, then +nil+ is returned.
59
- def get(key, options=nil)
62
+ def get(key, options = nil)
60
63
  perform(:get, key, options)
61
64
  end
62
65
 
@@ -65,15 +68,15 @@ module Dalli
65
68
  # If a block is given, yields key/value pairs one at a time.
66
69
  # Otherwise returns a hash of { 'key' => 'value', 'key2' => 'value1' }
67
70
  def get_multi(*keys)
68
- check_keys = keys.flatten
69
- check_keys.compact!
71
+ keys.flatten!
72
+ keys.compact!
70
73
 
71
- return {} if check_keys.empty?
74
+ return {} if keys.empty?
72
75
  if block_given?
73
- get_multi_yielder(keys) {|k, data| yield k, data.first}
76
+ get_multi_yielder(keys) { |k, data| yield k, data.first }
74
77
  else
75
- Hash.new.tap do |hash|
76
- get_multi_yielder(keys) {|k, data| hash[k] = data.first}
78
+ {}.tap do |hash|
79
+ get_multi_yielder(keys) { |k, data| hash[k] = data.first }
77
80
  end
78
81
  end
79
82
  end
@@ -88,11 +91,11 @@ module Dalli
88
91
  # If a value is not found (or if the found value is nil and :cache_nils is false)
89
92
  # and a block is given, the block will be invoked and its return value
90
93
  # written to the cache and returned.
91
- def fetch(key, ttl=nil, options=nil)
94
+ def fetch(key, ttl = nil, options = nil)
92
95
  options = options.nil? ? CACHE_NILS : options.merge(CACHE_NILS) if @options[:cache_nils]
93
96
  val = get(key, options)
94
97
  not_found = @options[:cache_nils] ?
95
- val == Dalli::Server::NOT_FOUND :
98
+ val == Dalli::Protocol::NOT_FOUND :
96
99
  val.nil?
97
100
  if not_found && block_given?
98
101
  val = yield
@@ -112,7 +115,7 @@ module Dalli
112
115
  # - nil if the key did not exist.
113
116
  # - false if the value was changed by someone else.
114
117
  # - true if the value was successfully updated.
115
- def cas(key, ttl=nil, options=nil, &block)
118
+ def cas(key, ttl = nil, options = nil, &block)
116
119
  cas_core(key, false, ttl, options, &block)
117
120
  end
118
121
 
@@ -123,25 +126,25 @@ module Dalli
123
126
  # Returns:
124
127
  # - false if the value was changed by someone else.
125
128
  # - true if the value was successfully updated.
126
- def cas!(key, ttl=nil, options=nil, &block)
129
+ def cas!(key, ttl = nil, options = nil, &block)
127
130
  cas_core(key, true, ttl, options, &block)
128
131
  end
129
132
 
130
- def set(key, value, ttl=nil, options=nil)
133
+ def set(key, value, ttl = nil, options = nil)
131
134
  perform(:set, key, value, ttl_or_default(ttl), 0, options)
132
135
  end
133
136
 
134
137
  ##
135
138
  # Conditionally add a key/value pair, if the key does not already exist
136
139
  # on the server. Returns truthy if the operation succeeded.
137
- def add(key, value, ttl=nil, options=nil)
140
+ def add(key, value, ttl = nil, options = nil)
138
141
  perform(:add, key, value, ttl_or_default(ttl), options)
139
142
  end
140
143
 
141
144
  ##
142
145
  # Conditionally add a key/value pair, only if the key already exists
143
146
  # on the server. Returns truthy if the operation succeeded.
144
- def replace(key, value, ttl=nil, options=nil)
147
+ def replace(key, value, ttl = nil, options = nil)
145
148
  perform(:replace, key, value, ttl_or_default(ttl), 0, options)
146
149
  end
147
150
 
@@ -163,7 +166,7 @@ module Dalli
163
166
  perform(:prepend, key, value.to_s)
164
167
  end
165
168
 
166
- def flush(delay=0)
169
+ def flush(delay = 0)
167
170
  time = -delay
168
171
  ring.servers.map { |s| s.request(:flush, time += delay) }
169
172
  end
@@ -181,7 +184,7 @@ module Dalli
181
184
  # Note that the ttl will only apply if the counter does not already
182
185
  # exist. To increase an existing counter and update its TTL, use
183
186
  # #cas.
184
- def incr(key, amt=1, ttl=nil, default=nil)
187
+ def incr(key, amt = 1, ttl = nil, default = nil)
185
188
  raise ArgumentError, "Positive values only: #{amt}" if amt < 0
186
189
  perform(:incr, key, amt.to_i, ttl_or_default(ttl), default)
187
190
  end
@@ -200,7 +203,7 @@ module Dalli
200
203
  # Note that the ttl will only apply if the counter does not already
201
204
  # exist. To decrease an existing counter and update its TTL, use
202
205
  # #cas.
203
- def decr(key, amt=1, ttl=nil, default=nil)
206
+ def decr(key, amt = 1, ttl = nil, default = nil)
204
207
  raise ArgumentError, "Positive values only: #{amt}" if amt < 0
205
208
  perform(:decr, key, amt.to_i, ttl_or_default(ttl), default)
206
209
  end
@@ -209,20 +212,28 @@ module Dalli
209
212
  # Touch updates expiration time for a given key.
210
213
  #
211
214
  # Returns true if key exists, otherwise nil.
212
- def touch(key, ttl=nil)
215
+ def touch(key, ttl = nil)
213
216
  resp = perform(:touch, key, ttl_or_default(ttl))
214
217
  resp.nil? ? nil : true
215
218
  end
216
219
 
220
+ ##
221
+ # Gat (get and touch) fetch an item and simultaneously update its expiration time.
222
+ #
223
+ # If a value is not found, then +nil+ is returned.
224
+ def gat(key, ttl = nil)
225
+ perform(:gat, key, ttl_or_default(ttl))
226
+ end
227
+
217
228
  ##
218
229
  # Collect the stats for each server.
219
230
  # You can optionally pass a type including :items, :slabs or :settings to get specific stats
220
231
  # Returns a hash like { 'hostname:port' => { 'stat1' => 'value1', ... }, 'hostname2:port' => { ... } }
221
- def stats(type=nil)
222
- type = nil if ![nil, :items,:slabs,:settings].include? type
232
+ def stats(type = nil)
233
+ type = nil unless [nil, :items, :slabs, :settings].include? type
223
234
  values = {}
224
235
  ring.servers.each do |server|
225
- values["#{server.name}"] = server.alive? ? server.request(:stats,type.to_s) : nil
236
+ values[server.name.to_s] = server.alive? ? server.request(:stats, type.to_s) : nil
226
237
  end
227
238
  values
228
239
  end
@@ -246,11 +257,63 @@ module Dalli
246
257
  def version
247
258
  values = {}
248
259
  ring.servers.each do |server|
249
- values["#{server.name}"] = server.alive? ? server.request(:version) : nil
260
+ values[server.name.to_s] = server.alive? ? server.request(:version) : nil
250
261
  end
251
262
  values
252
263
  end
253
264
 
265
+ ##
266
+ # Get the value and CAS ID associated with the key. If a block is provided,
267
+ # value and CAS will be passed to the block.
268
+ def get_cas(key)
269
+ (value, cas) = perform(:cas, key)
270
+ value = !value || value == "Not found" ? nil : value
271
+ if block_given?
272
+ yield value, cas
273
+ else
274
+ [value, cas]
275
+ end
276
+ end
277
+
278
+ ##
279
+ # Fetch multiple keys efficiently, including available metadata such as CAS.
280
+ # If a block is given, yields key/data pairs one a time. Data is an array:
281
+ # [value, cas_id]
282
+ # If no block is given, returns a hash of
283
+ # { 'key' => [value, cas_id] }
284
+ def get_multi_cas(*keys)
285
+ if block_given?
286
+ get_multi_yielder(keys) { |*args| yield(*args) }
287
+ else
288
+ {}.tap do |hash|
289
+ get_multi_yielder(keys) { |k, data| hash[k] = data }
290
+ end
291
+ end
292
+ end
293
+
294
+ ##
295
+ # Set the key-value pair, verifying existing CAS.
296
+ # Returns the resulting CAS value if succeeded, and falsy otherwise.
297
+ def set_cas(key, value, cas, ttl = nil, options = nil)
298
+ ttl ||= @options[:expires_in].to_i
299
+ perform(:set, key, value, ttl, cas, options)
300
+ end
301
+
302
+ ##
303
+ # Conditionally add a key/value pair, verifying existing CAS, only if the
304
+ # key already exists on the server. Returns the new CAS value if the
305
+ # operation succeeded, or falsy otherwise.
306
+ def replace_cas(key, value, cas, ttl = nil, options = nil)
307
+ ttl ||= @options[:expires_in].to_i
308
+ perform(:replace, key, value, ttl, cas, options)
309
+ end
310
+
311
+ # Delete a key/value pair, verifying existing CAS.
312
+ # Returns true if succeeded, and falsy otherwise.
313
+ def delete_cas(key, cas = 0)
314
+ perform(:delete, key, cas)
315
+ end
316
+
254
317
  ##
255
318
  # Close our connection to each server.
256
319
  # If you perform another operation after this, the connections will be re-established.
@@ -269,9 +332,9 @@ module Dalli
269
332
 
270
333
  private
271
334
 
272
- def cas_core(key, always_set, ttl=nil, options=nil)
335
+ def cas_core(key, always_set, ttl = nil, options = nil)
273
336
  (value, cas) = perform(:cas, key)
274
- value = (!value || value == 'Not found') ? nil : value
337
+ value = !value || value == "Not found" ? nil : value
275
338
  return if value.nil? && !always_set
276
339
  newvalue = yield(value)
277
340
  perform(:set, key, newvalue, ttl_or_default(ttl), cas, options)
@@ -284,50 +347,61 @@ module Dalli
284
347
  end
285
348
 
286
349
  def groups_for_keys(*keys)
287
- groups = mapped_keys(keys).flatten.group_by do |key|
350
+ keys.flatten!
351
+ keys.map! { |a| validate_key(a.to_s) }
352
+
353
+ keys.group_by { |key|
288
354
  begin
289
355
  ring.server_for_key(key)
290
356
  rescue Dalli::RingError
291
357
  Dalli.logger.debug { "unable to get key #{key}" }
292
358
  nil
293
359
  end
294
- end
295
- return groups
296
- end
297
-
298
- def mapped_keys(keys)
299
- keys_array = keys.flatten
300
- keys_array.map! { |a| validate_key(a.to_s) }
301
- keys_array
360
+ }
302
361
  end
303
362
 
304
363
  def make_multi_get_requests(groups)
305
364
  groups.each do |server, keys_for_server|
306
- begin
307
- # TODO: do this with the perform chokepoint?
308
- # But given the fact that fetching the response doesn't take place
309
- # in that slot it's misleading anyway. Need to move all of this method
310
- # into perform to be meaningful
311
- server.request(:send_multiget, keys_for_server)
312
- rescue DalliError, NetworkError => e
313
- Dalli.logger.debug { e.inspect }
314
- Dalli.logger.debug { "unable to get keys for server #{server.name}" }
315
- end
365
+ # TODO: do this with the perform chokepoint?
366
+ # But given the fact that fetching the response doesn't take place
367
+ # in that slot it's misleading anyway. Need to move all of this method
368
+ # into perform to be meaningful
369
+ server.request(:send_multiget, keys_for_server)
370
+ rescue DalliError, NetworkError => e
371
+ Dalli.logger.debug { e.inspect }
372
+ Dalli.logger.debug { "unable to get keys for server #{server.name}" }
316
373
  end
317
374
  end
318
375
 
319
376
  def perform_multi_response_start(servers)
377
+ deleted = []
378
+
320
379
  servers.each do |server|
321
380
  next unless server.alive?
381
+
322
382
  begin
323
383
  server.multi_response_start
324
- rescue DalliError, NetworkError => e
384
+ rescue Dalli::NetworkError
385
+ servers.each { |s| s.multi_response_abort unless s.sock.nil? }
386
+ raise
387
+ rescue Dalli::DalliError => e
325
388
  Dalli.logger.debug { e.inspect }
326
389
  Dalli.logger.debug { "results from this server will be missing" }
327
- servers.delete(server)
390
+ deleted.append(server)
328
391
  end
329
392
  end
330
- servers
393
+
394
+ servers.delete_if { |server| deleted.include?(server) }
395
+ end
396
+
397
+ ##
398
+ # Ensures that the servers arg is either an array or a string.
399
+ def validate_servers_arg(servers)
400
+ return if servers.nil?
401
+ return if servers.is_a?(Array)
402
+ return if servers.is_a?(String)
403
+
404
+ raise ArgumentError, "An explicit servers argument must be a comma separated string or an array containing strings."
331
405
  end
332
406
 
333
407
  ##
@@ -346,30 +420,30 @@ module Dalli
346
420
 
347
421
  def ring
348
422
  @ring ||= Dalli::Ring.new(
349
- @servers.map do |s|
350
- server_options = {}
351
- if s =~ %r{\Amemcached://}
423
+ @servers.map { |s|
424
+ server_options = {}
425
+ if s.start_with?("memcached://")
352
426
  uri = URI.parse(s)
353
427
  server_options[:username] = uri.user
354
428
  server_options[:password] = uri.password
355
429
  s = "#{uri.host}:#{uri.port}"
356
430
  end
357
- Dalli::Server.new(s, @options.merge(server_options))
358
- end, @options
431
+ @options.fetch(:protocol_implementation, Dalli::Protocol::Binary).new(s, @options.merge(server_options))
432
+ }, @options
359
433
  )
360
434
  end
361
435
 
362
436
  # Chokepoint method for instrumentation
363
437
  def perform(*all_args)
364
- return yield if block_given?
365
- op, key, *args = *all_args
366
-
367
- key = key.to_s
368
- key = validate_key(key)
369
438
  begin
439
+ return yield if block_given?
440
+ op, key, *args = all_args
441
+
442
+ key = key.to_s
443
+ key = validate_key(key)
444
+
370
445
  server = ring.server_for_key(key)
371
- ret = server.request(op, key, *args)
372
- ret
446
+ server.request(op, key, *args)
373
447
  rescue NetworkError => e
374
448
  Dalli.logger.debug { e.inspect }
375
449
  Dalli.logger.debug { "retrying request with new server" }
@@ -382,10 +456,10 @@ module Dalli
382
456
  key = key_with_namespace(key)
383
457
  if key.length > 250
384
458
  digest_class = @options[:digest_class] || ::Digest::MD5
385
- max_length_before_namespace = 212 - (namespace || '').size
459
+ max_length_before_namespace = 212 - (namespace || "").size
386
460
  key = "#{key[0, max_length_before_namespace]}:md5:#{digest_class.hexdigest(key)}"
387
461
  end
388
- return key
462
+ key
389
463
  end
390
464
 
391
465
  def key_with_namespace(key)
@@ -393,7 +467,7 @@ module Dalli
393
467
  end
394
468
 
395
469
  def key_without_namespace(key)
396
- (ns = namespace) ? key.sub(%r(\A#{Regexp.escape ns}:), '') : key
470
+ (ns = namespace) ? key.sub(%r{\A#{Regexp.escape ns}:}, "") : key
397
471
  end
398
472
 
399
473
  def namespace
@@ -423,54 +497,53 @@ module Dalli
423
497
  perform do
424
498
  return {} if keys.empty?
425
499
  ring.lock do
426
- begin
427
- groups = groups_for_keys(keys)
428
- if unfound_keys = groups.delete(nil)
429
- Dalli.logger.debug { "unable to get keys for #{unfound_keys.length} keys because no matching server was found" }
430
- end
431
- make_multi_get_requests(groups)
432
-
433
- servers = groups.keys
434
- return if servers.empty?
435
- servers = perform_multi_response_start(servers)
436
-
437
- start = Time.now
438
- while true
439
- # remove any dead servers
440
- servers.delete_if { |s| s.sock.nil? }
441
- break if servers.empty?
442
-
443
- # calculate remaining timeout
444
- elapsed = Time.now - start
445
- timeout = servers.first.options[:socket_timeout]
446
- time_left = (elapsed > timeout) ? 0 : timeout - elapsed
447
-
448
- sockets = servers.map(&:sock)
449
- readable, _ = IO.select(sockets, nil, nil, time_left)
450
-
451
- if readable.nil?
452
- # no response within timeout; abort pending connections
453
- servers.each do |server|
454
- Dalli.logger.debug { "memcached at #{server.name} did not response within timeout" }
455
- server.multi_response_abort
456
- end
457
- break
500
+ groups = groups_for_keys(keys)
501
+ if (unfound_keys = groups.delete(nil))
502
+ Dalli.logger.debug { "unable to get keys for #{unfound_keys.length} keys because no matching server was found" }
503
+ end
504
+ make_multi_get_requests(groups)
505
+
506
+ servers = groups.keys
507
+ return if servers.empty?
508
+ servers = perform_multi_response_start(servers)
509
+
510
+ start = Time.now
511
+ loop do
512
+ # remove any dead servers
513
+ servers.delete_if { |s| s.sock.nil? }
514
+ break if servers.empty?
515
+
516
+ # calculate remaining timeout
517
+ elapsed = Time.now - start
518
+ timeout = servers.first.options[:socket_timeout]
519
+ time_left = elapsed > timeout ? 0 : timeout - elapsed
520
+
521
+ sockets = servers.map(&:sock)
522
+ readable, _ = IO.select(sockets, nil, nil, time_left)
523
+
524
+ if readable.nil?
525
+ # no response within timeout; abort pending connections
526
+ servers.each do |server|
527
+ Dalli.logger.debug { "memcached at #{server.name} did not response within timeout" }
528
+ server.multi_response_abort
529
+ end
530
+ break
458
531
 
459
- else
460
- readable.each do |sock|
461
- server = sock.server
532
+ else
533
+ readable.each do |sock|
534
+ server = sock.server
462
535
 
463
- begin
464
- server.multi_response_nonblock.each_pair do |key, value_list|
465
- yield key_without_namespace(key), value_list
466
- end
536
+ begin
537
+ server.multi_response_nonblock.each_pair do |key, value_list|
538
+ yield key_without_namespace(key), value_list
539
+ end
467
540
 
468
- if server.multi_response_completed?
469
- servers.delete(server)
470
- end
471
- rescue NetworkError
541
+ if server.multi_response_completed?
472
542
  servers.delete(server)
473
543
  end
544
+ rescue NetworkError
545
+ servers.each { |s| s.multi_response_abort unless s.sock.nil? }
546
+ raise
474
547
  end
475
548
  end
476
549
  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)