dalli 2.7.11 → 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:
@@ -30,9 +30,11 @@ module Dalli
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
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.
33
34
  #
34
- def initialize(servers=nil, options={})
35
- @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")
36
38
  @options = normalize_options(options)
37
39
  @ring = nil
38
40
  end
@@ -56,7 +58,7 @@ module Dalli
56
58
  ##
57
59
  # Get the value associated with the key.
58
60
  # If a value is not found, then +nil+ is returned.
59
- def get(key, options=nil)
61
+ def get(key, options = nil)
60
62
  perform(:get, key, options)
61
63
  end
62
64
 
@@ -65,15 +67,15 @@ module Dalli
65
67
  # If a block is given, yields key/value pairs one at a time.
66
68
  # Otherwise returns a hash of { 'key' => 'value', 'key2' => 'value1' }
67
69
  def get_multi(*keys)
68
- check_keys = keys.flatten
69
- check_keys.compact!
70
+ keys.flatten!
71
+ keys.compact!
70
72
 
71
- return {} if check_keys.empty?
73
+ return {} if keys.empty?
72
74
  if block_given?
73
- get_multi_yielder(keys) {|k, data| yield k, data.first}
75
+ get_multi_yielder(keys) { |k, data| yield k, data.first }
74
76
  else
75
- Hash.new.tap do |hash|
76
- 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 }
77
79
  end
78
80
  end
79
81
  end
@@ -88,11 +90,11 @@ module Dalli
88
90
  # If a value is not found (or if the found value is nil and :cache_nils is false)
89
91
  # and a block is given, the block will be invoked and its return value
90
92
  # written to the cache and returned.
91
- def fetch(key, ttl=nil, options=nil)
93
+ def fetch(key, ttl = nil, options = nil)
92
94
  options = options.nil? ? CACHE_NILS : options.merge(CACHE_NILS) if @options[:cache_nils]
93
95
  val = get(key, options)
94
96
  not_found = @options[:cache_nils] ?
95
- val == Dalli::Server::NOT_FOUND :
97
+ val == Dalli::Protocol::NOT_FOUND :
96
98
  val.nil?
97
99
  if not_found && block_given?
98
100
  val = yield
@@ -112,7 +114,7 @@ module Dalli
112
114
  # - nil if the key did not exist.
113
115
  # - false if the value was changed by someone else.
114
116
  # - true if the value was successfully updated.
115
- def cas(key, ttl=nil, options=nil, &block)
117
+ def cas(key, ttl = nil, options = nil, &block)
116
118
  cas_core(key, false, ttl, options, &block)
117
119
  end
118
120
 
@@ -123,25 +125,25 @@ module Dalli
123
125
  # Returns:
124
126
  # - false if the value was changed by someone else.
125
127
  # - true if the value was successfully updated.
126
- def cas!(key, ttl=nil, options=nil, &block)
128
+ def cas!(key, ttl = nil, options = nil, &block)
127
129
  cas_core(key, true, ttl, options, &block)
128
130
  end
129
131
 
130
- def set(key, value, ttl=nil, options=nil)
132
+ def set(key, value, ttl = nil, options = nil)
131
133
  perform(:set, key, value, ttl_or_default(ttl), 0, options)
132
134
  end
133
135
 
134
136
  ##
135
137
  # Conditionally add a key/value pair, if the key does not already exist
136
138
  # on the server. Returns truthy if the operation succeeded.
137
- def add(key, value, ttl=nil, options=nil)
139
+ def add(key, value, ttl = nil, options = nil)
138
140
  perform(:add, key, value, ttl_or_default(ttl), options)
139
141
  end
140
142
 
141
143
  ##
142
144
  # Conditionally add a key/value pair, only if the key already exists
143
145
  # on the server. Returns truthy if the operation succeeded.
144
- def replace(key, value, ttl=nil, options=nil)
146
+ def replace(key, value, ttl = nil, options = nil)
145
147
  perform(:replace, key, value, ttl_or_default(ttl), 0, options)
146
148
  end
147
149
 
@@ -163,7 +165,7 @@ module Dalli
163
165
  perform(:prepend, key, value.to_s)
164
166
  end
165
167
 
166
- def flush(delay=0)
168
+ def flush(delay = 0)
167
169
  time = -delay
168
170
  ring.servers.map { |s| s.request(:flush, time += delay) }
169
171
  end
@@ -181,7 +183,7 @@ module Dalli
181
183
  # Note that the ttl will only apply if the counter does not already
182
184
  # exist. To increase an existing counter and update its TTL, use
183
185
  # #cas.
184
- def incr(key, amt=1, ttl=nil, default=nil)
186
+ def incr(key, amt = 1, ttl = nil, default = nil)
185
187
  raise ArgumentError, "Positive values only: #{amt}" if amt < 0
186
188
  perform(:incr, key, amt.to_i, ttl_or_default(ttl), default)
187
189
  end
@@ -200,7 +202,7 @@ module Dalli
200
202
  # Note that the ttl will only apply if the counter does not already
201
203
  # exist. To decrease an existing counter and update its TTL, use
202
204
  # #cas.
203
- def decr(key, amt=1, ttl=nil, default=nil)
205
+ def decr(key, amt = 1, ttl = nil, default = nil)
204
206
  raise ArgumentError, "Positive values only: #{amt}" if amt < 0
205
207
  perform(:decr, key, amt.to_i, ttl_or_default(ttl), default)
206
208
  end
@@ -209,20 +211,28 @@ module Dalli
209
211
  # Touch updates expiration time for a given key.
210
212
  #
211
213
  # Returns true if key exists, otherwise nil.
212
- def touch(key, ttl=nil)
214
+ def touch(key, ttl = nil)
213
215
  resp = perform(:touch, key, ttl_or_default(ttl))
214
216
  resp.nil? ? nil : true
215
217
  end
216
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
+
217
227
  ##
218
228
  # Collect the stats for each server.
219
229
  # You can optionally pass a type including :items, :slabs or :settings to get specific stats
220
230
  # 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
231
+ def stats(type = nil)
232
+ type = nil unless [nil, :items, :slabs, :settings].include? type
223
233
  values = {}
224
234
  ring.servers.each do |server|
225
- 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
226
236
  end
227
237
  values
228
238
  end
@@ -246,11 +256,63 @@ module Dalli
246
256
  def version
247
257
  values = {}
248
258
  ring.servers.each do |server|
249
- values["#{server.name}"] = server.alive? ? server.request(:version) : nil
259
+ values[server.name.to_s] = server.alive? ? server.request(:version) : nil
250
260
  end
251
261
  values
252
262
  end
253
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
+
254
316
  ##
255
317
  # Close our connection to each server.
256
318
  # If you perform another operation after this, the connections will be re-established.
@@ -262,16 +324,11 @@ module Dalli
262
324
  end
263
325
  alias_method :reset, :close
264
326
 
265
- # Stub method so a bare Dalli client can pretend to be a connection pool.
266
- def with
267
- yield self
268
- end
269
-
270
327
  private
271
328
 
272
- def cas_core(key, always_set, ttl=nil, options=nil)
329
+ def cas_core(key, always_set, ttl = nil, options = nil)
273
330
  (value, cas) = perform(:cas, key)
274
- value = (!value || value == 'Not found') ? nil : value
331
+ value = !value || value == "Not found" ? nil : value
275
332
  return if value.nil? && !always_set
276
333
  newvalue = yield(value)
277
334
  perform(:set, key, newvalue, ttl_or_default(ttl), cas, options)
@@ -284,35 +341,29 @@ module Dalli
284
341
  end
285
342
 
286
343
  def groups_for_keys(*keys)
287
- 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|
288
348
  begin
289
349
  ring.server_for_key(key)
290
350
  rescue Dalli::RingError
291
351
  Dalli.logger.debug { "unable to get key #{key}" }
292
352
  nil
293
353
  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
354
+ }
302
355
  end
303
356
 
304
357
  def make_multi_get_requests(groups)
305
358
  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
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}" }
316
367
  end
317
368
  end
318
369
 
@@ -330,6 +381,16 @@ module Dalli
330
381
  servers
331
382
  end
332
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
+
333
394
  ##
334
395
  # Normalizes the argument into an array of servers.
335
396
  # If the argument is a string, or an array containing strings, it's expected that the URIs are comma separated e.g.
@@ -346,30 +407,29 @@ module Dalli
346
407
 
347
408
  def ring
348
409
  @ring ||= Dalli::Ring.new(
349
- @servers.map do |s|
350
- server_options = {}
351
- if s =~ %r{\Amemcached://}
410
+ @servers.map { |s|
411
+ server_options = {}
412
+ if s.start_with?("memcached://")
352
413
  uri = URI.parse(s)
353
414
  server_options[:username] = uri.user
354
415
  server_options[:password] = uri.password
355
416
  s = "#{uri.host}:#{uri.port}"
356
417
  end
357
- Dalli::Server.new(s, @options.merge(server_options))
358
- end, @options
418
+ @options.fetch(:protocol_implementation, Dalli::Protocol::Binary).new(s, @options.merge(server_options))
419
+ }, @options
359
420
  )
360
421
  end
361
422
 
362
423
  # Chokepoint method for instrumentation
363
424
  def perform(*all_args)
364
425
  return yield if block_given?
365
- op, key, *args = *all_args
426
+ op, key, *args = all_args
366
427
 
367
428
  key = key.to_s
368
429
  key = validate_key(key)
369
430
  begin
370
431
  server = ring.server_for_key(key)
371
- ret = server.request(op, key, *args)
372
- ret
432
+ server.request(op, key, *args)
373
433
  rescue NetworkError => e
374
434
  Dalli.logger.debug { e.inspect }
375
435
  Dalli.logger.debug { "retrying request with new server" }
@@ -382,10 +442,10 @@ module Dalli
382
442
  key = key_with_namespace(key)
383
443
  if key.length > 250
384
444
  digest_class = @options[:digest_class] || ::Digest::MD5
385
- max_length_before_namespace = 212 - (namespace || '').size
445
+ max_length_before_namespace = 212 - (namespace || "").size
386
446
  key = "#{key[0, max_length_before_namespace]}:md5:#{digest_class.hexdigest(key)}"
387
447
  end
388
- return key
448
+ key
389
449
  end
390
450
 
391
451
  def key_with_namespace(key)
@@ -393,7 +453,7 @@ module Dalli
393
453
  end
394
454
 
395
455
  def key_without_namespace(key)
396
- (ns = namespace) ? key.sub(%r(\A#{Regexp.escape ns}:), '') : key
456
+ (ns = namespace) ? key.sub(%r{\A#{Regexp.escape ns}:}, "") : key
397
457
  end
398
458
 
399
459
  def namespace
@@ -423,54 +483,52 @@ module Dalli
423
483
  perform do
424
484
  return {} if keys.empty?
425
485
  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
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
458
517
 
459
- else
460
- readable.each do |sock|
461
- server = sock.server
518
+ else
519
+ readable.each do |sock|
520
+ server = sock.server
462
521
 
463
- begin
464
- server.multi_response_nonblock.each_pair do |key, value_list|
465
- yield key_without_namespace(key), value_list
466
- end
522
+ begin
523
+ server.multi_response_nonblock.each_pair do |key, value_list|
524
+ yield key_without_namespace(key), value_list
525
+ end
467
526
 
468
- if server.multi_response_completed?
469
- servers.delete(server)
470
- end
471
- rescue NetworkError
527
+ if server.multi_response_completed?
472
528
  servers.delete(server)
473
529
  end
530
+ rescue NetworkError
531
+ servers.delete(server)
474
532
  end
475
533
  end
476
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)