dalli 2.7.0 → 2.7.11

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,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'digest/md5'
2
3
  require 'set'
3
4
 
@@ -9,12 +10,15 @@ module Dalli
9
10
  # Dalli::Client is the main class which developers will use to interact with
10
11
  # the memcached server. Usage:
11
12
  #
12
- # Dalli::Client.new(['localhost:11211:10', 'cache-2.example.com:11211:5', '192.168.0.1:22122:5'],
13
+ # Dalli::Client.new(['localhost:11211:10', 'cache-2.example.com:11211:5', '192.168.0.1:22122:5', '/var/run/memcached/socket'],
13
14
  # :threadsafe => true, :failover => true, :expires_in => 300)
14
15
  #
15
16
  # servers is an Array of "host:port:weight" where weight allows you to distribute cache unevenly.
16
17
  # Both weight and port are optional. If you pass in nil, Dalli will use the <tt>MEMCACHE_SERVERS</tt>
17
- # environment variable or default to 'localhost:11211' if it is not present.
18
+ # environment variable or default to 'localhost:11211' if it is not present. Dalli also supports
19
+ # the ability to connect to Memcached on localhost through a UNIX socket. To use this functionality,
20
+ # use a full pathname (beginning with a slash character '/') in place of the "host:port" pair in
21
+ # the server configuration.
18
22
  #
19
23
  # Options:
20
24
  # - :namespace - prepend each key with this value to provide simple namespacing.
@@ -24,6 +28,8 @@ module Dalli
24
28
  # - :compress - defaults to false, if true Dalli will compress values larger than 1024 bytes before sending them to memcached.
25
29
  # - :serializer - defaults to Marshal
26
30
  # - :compressor - defaults to zlib
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.
27
33
  #
28
34
  def initialize(servers=nil, options={})
29
35
  @servers = normalize_servers(servers || ENV["MEMCACHE_SERVERS"] || '127.0.0.1:11211')
@@ -49,8 +55,9 @@ module Dalli
49
55
 
50
56
  ##
51
57
  # Get the value associated with the key.
58
+ # If a value is not found, then +nil+ is returned.
52
59
  def get(key, options=nil)
53
- perform(:get, key)
60
+ perform(:get, key, options)
54
61
  end
55
62
 
56
63
  ##
@@ -58,21 +65,38 @@ module Dalli
58
65
  # If a block is given, yields key/value pairs one at a time.
59
66
  # Otherwise returns a hash of { 'key' => 'value', 'key2' => 'value1' }
60
67
  def get_multi(*keys)
68
+ check_keys = keys.flatten
69
+ check_keys.compact!
70
+
71
+ return {} if check_keys.empty?
61
72
  if block_given?
62
73
  get_multi_yielder(keys) {|k, data| yield k, data.first}
63
74
  else
64
- Hash.new.tap do |hash|
75
+ Hash.new.tap do |hash|
65
76
  get_multi_yielder(keys) {|k, data| hash[k] = data.first}
66
77
  end
67
78
  end
68
79
  end
69
80
 
81
+ CACHE_NILS = {cache_nils: true}.freeze
82
+
83
+ # Fetch the value associated with the key.
84
+ # If a value is found, then it is returned.
85
+ #
86
+ # If a value is not found and no block is given, then nil is returned.
87
+ #
88
+ # If a value is not found (or if the found value is nil and :cache_nils is false)
89
+ # and a block is given, the block will be invoked and its return value
90
+ # written to the cache and returned.
70
91
  def fetch(key, ttl=nil, options=nil)
71
- ttl ||= @options[:expires_in].to_i
92
+ options = options.nil? ? CACHE_NILS : options.merge(CACHE_NILS) if @options[:cache_nils]
72
93
  val = get(key, options)
73
- if val.nil? && block_given?
94
+ not_found = @options[:cache_nils] ?
95
+ val == Dalli::Server::NOT_FOUND :
96
+ val.nil?
97
+ if not_found && block_given?
74
98
  val = yield
75
- add(key, val, ttl, options)
99
+ add(key, val, ttl_or_default(ttl), options)
76
100
  end
77
101
  val
78
102
  end
@@ -89,34 +113,36 @@ module Dalli
89
113
  # - false if the value was changed by someone else.
90
114
  # - true if the value was successfully updated.
91
115
  def cas(key, ttl=nil, options=nil, &block)
92
- ttl ||= @options[:expires_in].to_i
93
- (value, cas) = perform(:cas, key)
94
- value = (!value || value == 'Not found') ? nil : value
95
- if value
96
- newvalue = block.call(value)
97
- perform(:set, key, newvalue, ttl, cas, options)
98
- end
116
+ cas_core(key, false, ttl, options, &block)
117
+ end
118
+
119
+ ##
120
+ # like #cas, but will yield to the block whether or not the value
121
+ # already exists.
122
+ #
123
+ # Returns:
124
+ # - false if the value was changed by someone else.
125
+ # - true if the value was successfully updated.
126
+ def cas!(key, ttl=nil, options=nil, &block)
127
+ cas_core(key, true, ttl, options, &block)
99
128
  end
100
129
 
101
130
  def set(key, value, ttl=nil, options=nil)
102
- ttl ||= @options[:expires_in].to_i
103
- perform(:set, key, value, ttl, 0, options)
131
+ perform(:set, key, value, ttl_or_default(ttl), 0, options)
104
132
  end
105
133
 
106
134
  ##
107
135
  # Conditionally add a key/value pair, if the key does not already exist
108
- # on the server. Returns true if the operation succeeded.
136
+ # on the server. Returns truthy if the operation succeeded.
109
137
  def add(key, value, ttl=nil, options=nil)
110
- ttl ||= @options[:expires_in].to_i
111
- perform(:add, key, value, ttl, options)
138
+ perform(:add, key, value, ttl_or_default(ttl), options)
112
139
  end
113
140
 
114
141
  ##
115
142
  # Conditionally add a key/value pair, only if the key already exists
116
- # on the server. Returns true if the operation succeeded.
143
+ # on the server. Returns truthy if the operation succeeded.
117
144
  def replace(key, value, ttl=nil, options=nil)
118
- ttl ||= @options[:expires_in].to_i
119
- perform(:replace, key, value, ttl, 0, options)
145
+ perform(:replace, key, value, ttl_or_default(ttl), 0, options)
120
146
  end
121
147
 
122
148
  def delete(key)
@@ -157,8 +183,7 @@ module Dalli
157
183
  # #cas.
158
184
  def incr(key, amt=1, ttl=nil, default=nil)
159
185
  raise ArgumentError, "Positive values only: #{amt}" if amt < 0
160
- ttl ||= @options[:expires_in].to_i
161
- perform(:incr, key, amt.to_i, ttl, default)
186
+ perform(:incr, key, amt.to_i, ttl_or_default(ttl), default)
162
187
  end
163
188
 
164
189
  ##
@@ -177,8 +202,7 @@ module Dalli
177
202
  # #cas.
178
203
  def decr(key, amt=1, ttl=nil, default=nil)
179
204
  raise ArgumentError, "Positive values only: #{amt}" if amt < 0
180
- ttl ||= @options[:expires_in].to_i
181
- perform(:decr, key, amt.to_i, ttl, default)
205
+ perform(:decr, key, amt.to_i, ttl_or_default(ttl), default)
182
206
  end
183
207
 
184
208
  ##
@@ -186,20 +210,19 @@ module Dalli
186
210
  #
187
211
  # Returns true if key exists, otherwise nil.
188
212
  def touch(key, ttl=nil)
189
- ttl ||= @options[:expires_in].to_i
190
- resp = perform(:touch, key, ttl)
213
+ resp = perform(:touch, key, ttl_or_default(ttl))
191
214
  resp.nil? ? nil : true
192
215
  end
193
216
 
194
217
  ##
195
218
  # Collect the stats for each server.
196
- # You can optionally pass a type including :items or :slabs to get specific stats
219
+ # You can optionally pass a type including :items, :slabs or :settings to get specific stats
197
220
  # Returns a hash like { 'hostname:port' => { 'stat1' => 'value1', ... }, 'hostname2:port' => { ... } }
198
221
  def stats(type=nil)
199
- type = nil if ![nil, :items,:slabs].include? type
222
+ type = nil if ![nil, :items,:slabs,:settings].include? type
200
223
  values = {}
201
224
  ring.servers.each do |server|
202
- values["#{server.hostname}:#{server.port}"] = server.alive? ? server.request(:stats,type.to_s) : nil
225
+ values["#{server.name}"] = server.alive? ? server.request(:stats,type.to_s) : nil
203
226
  end
204
227
  values
205
228
  end
@@ -212,12 +235,18 @@ module Dalli
212
235
  end
213
236
  end
214
237
 
238
+ ##
239
+ ## Make sure memcache servers are alive, or raise an Dalli::RingError
240
+ def alive!
241
+ ring.server_for_key("")
242
+ end
243
+
215
244
  ##
216
245
  ## Version of the memcache servers.
217
246
  def version
218
247
  values = {}
219
248
  ring.servers.each do |server|
220
- values["#{server.hostname}:#{server.port}"] = server.alive? ? server.request(:version) : nil
249
+ values["#{server.name}"] = server.alive? ? server.request(:version) : nil
221
250
  end
222
251
  values
223
252
  end
@@ -240,6 +269,20 @@ module Dalli
240
269
 
241
270
  private
242
271
 
272
+ def cas_core(key, always_set, ttl=nil, options=nil)
273
+ (value, cas) = perform(:cas, key)
274
+ value = (!value || value == 'Not found') ? nil : value
275
+ return if value.nil? && !always_set
276
+ newvalue = yield(value)
277
+ perform(:set, key, newvalue, ttl_or_default(ttl), cas, options)
278
+ end
279
+
280
+ def ttl_or_default(ttl)
281
+ (ttl || @options[:expires_in]).to_i
282
+ rescue NoMethodError
283
+ raise ArgumentError, "Cannot convert ttl (#{ttl}) to an integer"
284
+ end
285
+
243
286
  def groups_for_keys(*keys)
244
287
  groups = mapped_keys(keys).flatten.group_by do |key|
245
288
  begin
@@ -253,7 +296,9 @@ module Dalli
253
296
  end
254
297
 
255
298
  def mapped_keys(keys)
256
- keys.flatten.map {|a| validate_key(a.to_s)}
299
+ keys_array = keys.flatten
300
+ keys_array.map! { |a| validate_key(a.to_s) }
301
+ keys_array
257
302
  end
258
303
 
259
304
  def make_multi_get_requests(groups)
@@ -266,7 +311,7 @@ module Dalli
266
311
  server.request(:send_multiget, keys_for_server)
267
312
  rescue DalliError, NetworkError => e
268
313
  Dalli.logger.debug { e.inspect }
269
- Dalli.logger.debug { "unable to get keys for server #{server.hostname}:#{server.port}" }
314
+ Dalli.logger.debug { "unable to get keys for server #{server.name}" }
270
315
  end
271
316
  end
272
317
  end
@@ -286,13 +331,16 @@ module Dalli
286
331
  end
287
332
 
288
333
  ##
289
- # Normalizes the argument into an array of servers. If the argument is a string, it's expected to be of
290
- # the format "memcache1.example.com:11211[,memcache2.example.com:11211[,memcache3.example.com:11211[...]]]
334
+ # Normalizes the argument into an array of servers.
335
+ # If the argument is a string, or an array containing strings, it's expected that the URIs are comma separated e.g.
336
+ # "memcache1.example.com:11211,memcache2.example.com:11211,memcache3.example.com:11211"
291
337
  def normalize_servers(servers)
292
- if servers.is_a? String
293
- return servers.split(",")
294
- else
295
- return servers
338
+ Array(servers).flat_map do |server|
339
+ if server.is_a? String
340
+ server.split(",")
341
+ else
342
+ server
343
+ end
296
344
  end
297
345
  end
298
346
 
@@ -312,8 +360,8 @@ module Dalli
312
360
  end
313
361
 
314
362
  # Chokepoint method for instrumentation
315
- def perform(*all_args, &blk)
316
- return blk.call if blk
363
+ def perform(*all_args)
364
+ return yield if block_given?
317
365
  op, key, *args = *all_args
318
366
 
319
367
  key = key.to_s
@@ -333,8 +381,9 @@ module Dalli
333
381
  raise ArgumentError, "key cannot be blank" if !key || key.length == 0
334
382
  key = key_with_namespace(key)
335
383
  if key.length > 250
384
+ digest_class = @options[:digest_class] || ::Digest::MD5
336
385
  max_length_before_namespace = 212 - (namespace || '').size
337
- key = "#{key[0, max_length_before_namespace]}:md5:#{Digest::MD5.hexdigest(key)}"
386
+ key = "#{key[0, max_length_before_namespace]}:md5:#{digest_class.hexdigest(key)}"
338
387
  end
339
388
  return key
340
389
  end
@@ -344,11 +393,12 @@ module Dalli
344
393
  end
345
394
 
346
395
  def key_without_namespace(key)
347
- (ns = namespace) ? key.sub(%r(\A#{ns}:), '') : key
396
+ (ns = namespace) ? key.sub(%r(\A#{Regexp.escape ns}:), '') : key
348
397
  end
349
398
 
350
399
  def namespace
351
- @options[:namespace].is_a?(Proc) ? @options[:namespace].call : @options[:namespace]
400
+ return nil unless @options[:namespace]
401
+ @options[:namespace].is_a?(Proc) ? @options[:namespace].call.to_s : @options[:namespace].to_s
352
402
  end
353
403
 
354
404
  def normalize_options(opts)
@@ -361,6 +411,9 @@ module Dalli
361
411
  rescue NoMethodError
362
412
  raise ArgumentError, "cannot convert :expires_in => #{opts[:expires_in].inspect} to an integer"
363
413
  end
414
+ if opts[:digest_class] && !opts[:digest_class].respond_to?(:hexdigest)
415
+ raise ArgumentError, "The digest_class object must respond to the hexdigest method"
416
+ end
364
417
  opts
365
418
  end
366
419
 
@@ -382,7 +435,7 @@ module Dalli
382
435
  servers = perform_multi_response_start(servers)
383
436
 
384
437
  start = Time.now
385
- loop do
438
+ while true
386
439
  # remove any dead servers
387
440
  servers.delete_if { |s| s.sock.nil? }
388
441
  break if servers.empty?
@@ -390,12 +443,10 @@ module Dalli
390
443
  # calculate remaining timeout
391
444
  elapsed = Time.now - start
392
445
  timeout = servers.first.options[:socket_timeout]
393
- if elapsed > timeout
394
- readable = nil
395
- else
396
- sockets = servers.map(&:sock)
397
- readable, _ = IO.select(sockets, nil, nil, timeout - elapsed)
398
- end
446
+ time_left = (elapsed > timeout) ? 0 : timeout - elapsed
447
+
448
+ sockets = servers.map(&:sock)
449
+ readable, _ = IO.select(sockets, nil, nil, time_left)
399
450
 
400
451
  if readable.nil?
401
452
  # no response within timeout; abort pending connections
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'zlib'
2
3
  require 'stringio'
3
4
 
@@ -14,7 +15,7 @@ module Dalli
14
15
 
15
16
  class GzipCompressor
16
17
  def self.compress(data)
17
- io = StringIO.new("w")
18
+ io = StringIO.new(String.new(""), "w")
18
19
  gz = Zlib::GzipWriter.new(io)
19
20
  gz.write(data)
20
21
  gz.close
data/lib/dalli/options.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'thread'
2
3
  require 'monitor'
3
4
 
data/lib/dalli/railtie.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Dalli
2
3
  class Railtie < ::Rails::Railtie
3
4
  config.before_configuration do
data/lib/dalli/ring.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'digest/sha1'
2
3
  require 'zlib'
3
4
 
@@ -15,12 +16,12 @@ module Dalli
15
16
  continuum = []
16
17
  servers.each do |server|
17
18
  entry_count_for(server, servers.size, total_weight).times do |idx|
18
- hash = Digest::SHA1.hexdigest("#{server.hostname}:#{server.port}:#{idx}")
19
+ hash = Digest::SHA1.hexdigest("#{server.name}:#{idx}")
19
20
  value = Integer("0x#{hash[0..7]}")
20
21
  continuum << Dalli::Ring::Entry.new(value, server)
21
22
  end
22
23
  end
23
- @continuum = continuum.sort { |a, b| a.value <=> b.value }
24
+ @continuum = continuum.sort_by(&:value)
24
25
  end
25
26
 
26
27
  threadsafe! unless options[:threadsafe] == false
@@ -46,11 +47,11 @@ module Dalli
46
47
  end
47
48
 
48
49
  def lock
49
- @servers.each { |s| s.lock! }
50
+ @servers.each(&:lock!)
50
51
  begin
51
52
  return yield
52
53
  ensure
53
- @servers.each { |s| s.unlock! }
54
+ @servers.each(&:unlock!)
54
55
  end
55
56
  end
56
57
 
@@ -110,7 +111,6 @@ module Dalli
110
111
  def binary_search(ary, value)
111
112
  upper = ary.size - 1
112
113
  lower = 0
113
- idx = 0
114
114
 
115
115
  while (lower <= upper) do
116
116
  idx = (lower + upper) / 2
@@ -124,7 +124,7 @@ module Dalli
124
124
  lower = idx + 1
125
125
  end
126
126
  end
127
- return upper
127
+ upper
128
128
  end
129
129
  end
130
130