dalli 1.1.4 → 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.

@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'active_support/cache'
2
3
  require 'action_dispatch/middleware/session/abstract_store'
3
4
  require 'dalli'
@@ -20,6 +21,8 @@ module ActionDispatch
20
21
  end
21
22
  @namespace = @default_options[:namespace]
22
23
 
24
+ @raise_errors = !!@default_options[:raise_errors]
25
+
23
26
  super
24
27
  end
25
28
 
@@ -30,7 +33,7 @@ module ActionDispatch
30
33
  private
31
34
 
32
35
  def get_session(env, sid)
33
- sid ||= generate_sid
36
+ sid = generate_sid unless sid and !sid.empty?
34
37
  begin
35
38
  session = @pool.get(sid) || {}
36
39
  rescue Dalli::DalliError => ex
@@ -49,6 +52,7 @@ module ActionDispatch
49
52
  sid
50
53
  rescue Dalli::DalliError
51
54
  Rails.logger.warn("Session::DalliStore#set: #{$!.message}")
55
+ raise if @raise_errors
52
56
  false
53
57
  end
54
58
 
@@ -57,6 +61,7 @@ module ActionDispatch
57
61
  @pool.delete(session_id)
58
62
  rescue Dalli::DalliError
59
63
  Rails.logger.warn("Session::DalliStore#destroy_session: #{$!.message}")
64
+ raise if @raise_errors
60
65
  end
61
66
  return nil if options[:drop]
62
67
  generate_sid
@@ -68,6 +73,7 @@ module ActionDispatch
68
73
  end
69
74
  rescue Dalli::DalliError
70
75
  Rails.logger.warn("Session::DalliStore#destroy: #{$!.message}")
76
+ raise if @raise_errors
71
77
  false
72
78
  end
73
79
 
@@ -1,24 +1,33 @@
1
1
  # encoding: ascii
2
- begin
3
- require 'dalli'
4
- rescue LoadError => e
5
- $stderr.puts "You don't have dalli installed in your application. Please add it to your Gemfile and run bundle install"
6
- raise e
7
- end
8
- require 'digest/md5'
9
- require 'active_support/cache'
2
+ # frozen_string_literal: true
3
+ require 'dalli'
10
4
 
11
5
  module ActiveSupport
12
6
  module Cache
13
- # A cache store implementation which stores data in Memcached:
14
- # http://www.memcached.org
15
- #
16
- # DalliStore implements the Strategy::LocalCache strategy which implements
17
- # an in memory cache inside of a block.
18
- class DalliStore < Store
7
+ class DalliStore
8
+
9
+ attr_reader :silence, :options
10
+ alias_method :silence?, :silence
11
+
12
+ def self.supports_cache_versioning?
13
+ true
14
+ end
15
+
16
+ # Silence the logger.
17
+ def silence!
18
+ @silence = true
19
+ self
20
+ end
21
+
22
+ # Silence the logger within a block.
23
+ def mute
24
+ previous_silence, @silence = defined?(@silence) && @silence, true
25
+ yield
26
+ ensure
27
+ @silence = previous_silence
28
+ end
19
29
 
20
30
  ESCAPE_KEY_CHARS = /[\x00-\x20%\x7F-\xFF]/
21
- RAW = { :raw => true }
22
31
 
23
32
  # Creates a new DalliStore object, with the given memcached server
24
33
  # addresses. Each address is either a host name, or a host-with-port string
@@ -29,35 +38,196 @@ module ActiveSupport
29
38
  # If no addresses are specified, then DalliStore will connect to
30
39
  # localhost port 11211 (the default memcached port).
31
40
  #
41
+ # Connection Pool support
42
+ #
43
+ # If you are using multithreaded Rails, the Rails.cache singleton can become a source
44
+ # of contention. You can use a connection pool of Dalli clients with Rails.cache by
45
+ # passing :pool_size and/or :pool_timeout:
46
+ #
47
+ # config.cache_store = :dalli_store, 'localhost:11211', :pool_size => 10
48
+ #
49
+ # Both pool options default to 5. You must include the `connection_pool` gem if you
50
+ # wish to use pool support.
51
+ #
32
52
  def initialize(*addresses)
53
+ puts <<-EOS
54
+ DEPRECATION: :dalli_store will be removed in Dalli 3.0.
55
+ Please use Rails' official :mem_cache_store instead.
56
+ https://guides.rubyonrails.org/caching_with_rails.html
57
+ EOS
33
58
  addresses = addresses.flatten
34
59
  options = addresses.extract_options!
35
- super(options)
60
+ @options = options.dup
61
+
62
+ pool_options = {}
63
+ pool_options[:size] = options[:pool_size] if options[:pool_size]
64
+ pool_options[:timeout] = options[:pool_timeout] if options[:pool_timeout]
36
65
 
37
- addresses << 'localhost:11211' if addresses.empty?
38
- options = options.dup
39
- # Extend expiry by stale TTL or else memcached will never return stale data.
40
- # See ActiveSupport::Cache#fetch.
41
- options[:expires_in] += options[:race_condition_ttl] if options[:expires_in] && options[:race_condition_ttl]
42
- @data = Dalli::Client.new(addresses, options)
66
+ @options[:compress] ||= @options[:compression]
67
+
68
+ addresses.compact!
69
+ servers = if addresses.empty?
70
+ nil # use the default from Dalli::Client
71
+ else
72
+ addresses
73
+ end
74
+ if pool_options.empty?
75
+ @data = Dalli::Client.new(servers, @options)
76
+ else
77
+ @data = ::ConnectionPool.new(pool_options) { Dalli::Client.new(servers, @options.merge(:threadsafe => false)) }
78
+ end
43
79
 
44
80
  extend Strategy::LocalCache
45
- extend LocalCacheWithRaw
81
+ extend LocalCacheEntryUnwrapAndRaw
82
+ end
83
+
84
+ ##
85
+ # Access the underlying Dalli::Client or ConnectionPool instance for
86
+ # access to get_multi, etc.
87
+ def dalli
88
+ @data
89
+ end
90
+
91
+ def with(&block)
92
+ @data.with(&block)
93
+ end
94
+
95
+ # Fetch the value associated with the key.
96
+ # If a value is found, then it is returned.
97
+ #
98
+ # If a value is not found and no block is given, then nil is returned.
99
+ #
100
+ # If a value is not found (or if the found value is nil and :cache_nils is false)
101
+ # and a block is given, the block will be invoked and its return value
102
+ # written to the cache and returned.
103
+ def fetch(name, options=nil)
104
+ options ||= {}
105
+ options[:cache_nils] = true if @options[:cache_nils]
106
+ namespaced_name = namespaced_key(name, options)
107
+ not_found = options[:cache_nils] ? Dalli::Server::NOT_FOUND : nil
108
+ if block_given?
109
+ entry = not_found
110
+ unless options[:force]
111
+ entry = instrument_with_log(:read, namespaced_name, options) do |payload|
112
+ read_entry(namespaced_name, options).tap do |result|
113
+ if payload
114
+ payload[:super_operation] = :fetch
115
+ payload[:hit] = not_found != result
116
+ end
117
+ end
118
+ end
119
+ end
120
+
121
+ if not_found == entry
122
+ result = instrument_with_log(:generate, namespaced_name, options) do |payload|
123
+ yield(name)
124
+ end
125
+ write(name, result, options)
126
+ result
127
+ else
128
+ instrument_with_log(:fetch_hit, namespaced_name, options) { |payload| }
129
+ entry
130
+ end
131
+ else
132
+ read(name, options)
133
+ end
134
+ end
135
+
136
+ def read(name, options=nil)
137
+ options ||= {}
138
+ name = namespaced_key(name, options)
139
+
140
+ instrument_with_log(:read, name, options) do |payload|
141
+ entry = read_entry(name, options)
142
+ payload[:hit] = !entry.nil? if payload
143
+ entry
144
+ end
145
+ end
146
+
147
+ def write(name, value, options=nil)
148
+ options ||= {}
149
+ name = namespaced_key(name, options)
150
+
151
+ instrument_with_log(:write, name, options) do |payload|
152
+ with do |connection|
153
+ options = options.merge(:connection => connection)
154
+ write_entry(name, value, options)
155
+ end
156
+ end
157
+ end
158
+
159
+ def exist?(name, options=nil)
160
+ options ||= {}
161
+ name = namespaced_key(name, options)
162
+
163
+ log(:exist, name, options)
164
+ !read_entry(name, options).nil?
165
+ end
166
+
167
+ def delete(name, options=nil)
168
+ options ||= {}
169
+ name = namespaced_key(name, options)
170
+
171
+ instrument_with_log(:delete, name, options) do |payload|
172
+ delete_entry(name, options)
173
+ end
46
174
  end
47
175
 
48
176
  # Reads multiple keys from the cache using a single call to the
49
- # servers for all keys. Options can be passed in the last argument.
177
+ # servers for all keys. Keys must be Strings.
50
178
  def read_multi(*names)
179
+ options = names.extract_options!
180
+ mapping = names.inject({}) { |memo, name| memo[namespaced_key(name, options)] = name; memo }
181
+ instrument_with_log(:read_multi, mapping.keys) do
182
+ results = {}
183
+ if local_cache
184
+ mapping.each_key do |key|
185
+ if value = local_cache.read_entry(key, options)
186
+ results[key] = value
187
+ end
188
+ end
189
+ end
190
+
191
+ data = with { |c| c.get_multi(mapping.keys - results.keys) }
192
+ results.merge!(data)
193
+ results.inject({}) do |memo, (inner, _)|
194
+ entry = results[inner]
195
+ # NB Backwards data compatibility, to be removed at some point
196
+ value = (entry.is_a?(ActiveSupport::Cache::Entry) ? entry.value : entry)
197
+ memo[mapping[inner]] = value
198
+ local_cache.write_entry(inner, value, options) if local_cache
199
+ memo
200
+ end
201
+ end
202
+ end
203
+
204
+ # Fetches data from the cache, using the given keys. If there is data in
205
+ # the cache with the given keys, then that data is returned. Otherwise,
206
+ # the supplied block is called for each key for which there was no data,
207
+ # and the result will be written to the cache and returned.
208
+ def fetch_multi(*names)
51
209
  options = names.extract_options!
52
- options = merged_options(options)
53
- keys_to_names = names.flatten.inject({}){|map, name| map[escape_key(namespaced_key(name, options))] = name; map}
54
- raw_values = @data.get_multi(keys_to_names.keys, RAW)
55
- values = {}
56
- raw_values.each do |key, value|
57
- entry = deserialize_entry(value)
58
- values[keys_to_names[key]] = entry.value unless entry.expired?
210
+ mapping = names.inject({}) { |memo, name| memo[namespaced_key(name, options)] = name; memo }
211
+
212
+ instrument_with_log(:fetch_multi, mapping.keys) do
213
+ with do |connection|
214
+ results = connection.get_multi(mapping.keys)
215
+
216
+ connection.multi do
217
+ mapping.inject({}) do |memo, (expanded, name)|
218
+ memo[name] = results[expanded]
219
+ if memo[name].nil?
220
+ value = yield(name)
221
+ memo[name] = value
222
+ options = options.merge(:connection => connection)
223
+ write_entry(expanded, value, options)
224
+ end
225
+
226
+ memo
227
+ end
228
+ end
229
+ end
59
230
  end
60
- values
61
231
  end
62
232
 
63
233
  # Increment a cached value. This method uses the memcached incr atomic
@@ -65,15 +235,18 @@ module ActiveSupport
65
235
  # Calling it on a value not stored with :raw will fail.
66
236
  # :initial defaults to the amount passed in, as if the counter was initially zero.
67
237
  # memcached counters cannot hold negative values.
68
- def increment(name, amount = 1, options = nil) # :nodoc:
69
- options = merged_options(options)
70
- initial = options[:initial] || amount
71
- expires_in = options[:expires_in].to_i
72
- response = instrument(:increment, name, :amount => amount) do
73
- @data.incr(escape_key(namespaced_key(name, options)), amount, expires_in, initial)
238
+ def increment(name, amount = 1, options=nil)
239
+ options ||= {}
240
+ name = namespaced_key(name, options)
241
+ initial = options.has_key?(:initial) ? options[:initial] : amount
242
+ expires_in = options[:expires_in]
243
+ instrument_with_log(:increment, name, :amount => amount) do
244
+ with { |c| c.incr(name, amount, expires_in, initial) }
74
245
  end
75
246
  rescue Dalli::DalliError => e
76
- logger.error("DalliError: #{e.message}") if logger
247
+ log_dalli_error(e)
248
+ instrument_error(e) if instrument_errors?
249
+ raise if raise_errors?
77
250
  nil
78
251
  end
79
252
 
@@ -82,101 +255,185 @@ module ActiveSupport
82
255
  # Calling it on a value not stored with :raw will fail.
83
256
  # :initial defaults to zero, as if the counter was initially zero.
84
257
  # memcached counters cannot hold negative values.
85
- def decrement(name, amount = 1, options = nil) # :nodoc:
86
- options = merged_options(options)
87
- initial = options[:initial] || 0
88
- expires_in = options[:expires_in].to_i
89
- response = instrument(:decrement, name, :amount => amount) do
90
- @data.decr(escape_key(namespaced_key(name, options)), amount, expires_in, initial)
258
+ def decrement(name, amount = 1, options=nil)
259
+ options ||= {}
260
+ name = namespaced_key(name, options)
261
+ initial = options.has_key?(:initial) ? options[:initial] : 0
262
+ expires_in = options[:expires_in]
263
+ instrument_with_log(:decrement, name, :amount => amount) do
264
+ with { |c| c.decr(name, amount, expires_in, initial) }
91
265
  end
92
266
  rescue Dalli::DalliError => e
93
- logger.error("DalliError: #{e.message}") if logger
267
+ log_dalli_error(e)
268
+ instrument_error(e) if instrument_errors?
269
+ raise if raise_errors?
94
270
  nil
95
271
  end
96
272
 
97
273
  # Clear the entire cache on all memcached servers. This method should
98
274
  # be used with care when using a shared cache.
99
- def clear(options = nil)
100
- @data.flush_all
275
+ def clear(options=nil)
276
+ instrument_with_log(:clear, 'flushing all keys') do
277
+ with { |c| c.flush_all }
278
+ end
279
+ rescue Dalli::DalliError => e
280
+ log_dalli_error(e)
281
+ instrument_error(e) if instrument_errors?
282
+ raise if raise_errors?
283
+ nil
284
+ end
285
+
286
+ # Clear any local cache
287
+ def cleanup(options=nil)
101
288
  end
102
289
 
103
290
  # Get the statistics from the memcached servers.
104
291
  def stats
105
- @data.stats
292
+ with { |c| c.stats }
106
293
  end
107
294
 
108
295
  def reset
109
- @data.reset
296
+ with { |c| c.reset }
297
+ end
298
+
299
+ def logger
300
+ Dalli.logger
301
+ end
302
+
303
+ def logger=(new_logger)
304
+ Dalli.logger = new_logger
110
305
  end
111
306
 
112
307
  protected
113
308
 
114
- # This CacheStore impl controls value marshalling so we take special
115
- # care to always pass :raw => true to the Dalli API so it does not
116
- # double marshal.
309
+ # Read an entry from the cache.
310
+ def read_entry(key, options) # :nodoc:
311
+ entry = with { |c| c.get(key, options) }
312
+ # NB Backwards data compatibility, to be removed at some point
313
+ entry.is_a?(ActiveSupport::Cache::Entry) ? entry.value : entry
314
+ rescue Dalli::DalliError => e
315
+ log_dalli_error(e)
316
+ instrument_error(e) if instrument_errors?
317
+ raise if raise_errors?
318
+ nil
319
+ end
320
+
321
+ # Write an entry to the cache.
322
+ def write_entry(key, value, options) # :nodoc:
323
+ # cleanup LocalCache
324
+ cleanup if options[:unless_exist]
325
+ method = options[:unless_exist] ? :add : :set
326
+ expires_in = options[:expires_in]
327
+ connection = options.delete(:connection)
328
+ connection.send(method, key, value, expires_in, options)
329
+ rescue Dalli::DalliError => e
330
+ log_dalli_error(e)
331
+ instrument_error(e) if instrument_errors?
332
+ raise if raise_errors?
333
+ false
334
+ end
117
335
 
118
- # Read an entry from the cache.
119
- def read_entry(key, options) # :nodoc:
120
- deserialize_entry(@data.get(escape_key(key), RAW))
121
- rescue Dalli::DalliError => e
122
- logger.error("DalliError: #{e.message}") if logger
123
- nil
124
- end
336
+ # Delete an entry from the cache.
337
+ def delete_entry(key, options) # :nodoc:
338
+ with { |c| c.delete(key) }
339
+ rescue Dalli::DalliError => e
340
+ log_dalli_error(e)
341
+ instrument_error(e) if instrument_errors?
342
+ raise if raise_errors?
343
+ false
344
+ end
345
+
346
+ private
125
347
 
126
- # Write an entry to the cache.
127
- def write_entry(key, entry, options) # :nodoc:
128
- method = options[:unless_exist] ? :add : :set
129
- value = options[:raw] ? entry.value.to_s : entry
130
- expires_in = options[:expires_in].to_i
131
- if expires_in > 0 && !options[:raw]
132
- # Set the memcache expire a few minutes in the future to support race condition ttls on read
133
- expires_in += 5.minutes
348
+ def namespaced_key(key, options)
349
+ digest_class = @options[:digest_class] || ::Digest::MD5
350
+ key = expanded_key(key)
351
+ namespace = options[:namespace] if options
352
+ prefix = namespace.is_a?(Proc) ? namespace.call : namespace
353
+ key = "#{prefix}:#{key}" if prefix
354
+ key = "#{key[0, 213]}:md5:#{digest_class.hexdigest(key)}" if key && key.size > 250
355
+ key
356
+ end
357
+ alias :normalize_key :namespaced_key
358
+
359
+ # Expand key to be a consistent string value. Invokes +cache_key_with_version+
360
+ # first to support Rails 5.2 cache versioning.
361
+ # Invoke +cache_key+ if object responds to +cache_key+. Otherwise, to_param method
362
+ # will be called. If the key is a Hash, then keys will be sorted alphabetically.
363
+ def expanded_key(key) # :nodoc:
364
+ return key.cache_key_with_version.to_s if key.respond_to?(:cache_key_with_version)
365
+ return key.cache_key.to_s if key.respond_to?(:cache_key)
366
+
367
+ case key
368
+ when Array
369
+ if key.size > 1
370
+ key = key.collect{|element| expanded_key(element)}
371
+ else
372
+ key = key.first
134
373
  end
135
- @data.send(method, escape_key(key), value, expires_in, options)
136
- rescue Dalli::DalliError => e
137
- logger.error("DalliError: #{e.message}") if logger
138
- false
374
+ when Hash
375
+ key = key.sort_by { |k,_| k.to_s }.collect{|k,v| "#{k}=#{v}"}
139
376
  end
140
377
 
141
- # Delete an entry from the cache.
142
- def delete_entry(key, options) # :nodoc:
143
- @data.delete(escape_key(key))
144
- rescue Dalli::DalliError => e
145
- logger.error("DalliError: #{e.message}") if logger
146
- false
378
+ key = key.to_param
379
+ if key.respond_to? :force_encoding
380
+ key = key.dup
381
+ key.force_encoding('binary')
147
382
  end
383
+ key
384
+ end
148
385
 
149
- private
150
- def escape_key(key)
151
- key = key.to_s
152
- key = key.force_encoding('ASCII-8BIT') if key.respond_to? :force_encoding
153
- key = key.gsub(ESCAPE_KEY_CHARS){|match| "%#{match.getbyte(0).to_s(16).upcase}"}
154
- key = "#{key[0, 213]}:md5:#{Digest::MD5.hexdigest(key)}" if key.size > 250
155
- key
156
- end
386
+ def log_dalli_error(error)
387
+ logger.error("DalliError: #{error.message}") if logger
388
+ end
157
389
 
158
- def deserialize_entry(raw_value)
159
- if raw_value
160
- # FIXME: This is a terrible implementation for performance reasons:
161
- # throwing an exception is much slower than some if logic.
162
- entry = Marshal.load(raw_value) rescue raw_value
163
- entry.is_a?(Entry) ? entry : Entry.new(entry)
164
- else
165
- nil
166
- end
390
+ def instrument_with_log(operation, key, options=nil)
391
+ log(operation, key, options)
392
+
393
+ payload = { :key => key }
394
+ payload.merge!(options) if options.is_a?(Hash)
395
+ instrument(operation, payload) { |p| yield(p) }
396
+ end
397
+
398
+ def instrument_error(error)
399
+ instrument(:error, { :key => 'DalliError', :message => error.message })
400
+ end
401
+
402
+ def instrument(operation, payload)
403
+ ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload) do
404
+ yield(payload) if block_given?
167
405
  end
406
+ end
407
+
408
+ def log(operation, key, options=nil)
409
+ return unless logger && logger.debug? && !silence?
410
+ logger.debug("Cache #{operation}: #{key}#{options.blank? ? "" : " (#{options.inspect})"}")
411
+ end
412
+
413
+ def raise_errors?
414
+ !!@options[:raise_errors]
415
+ end
416
+
417
+ def instrument_errors?
418
+ !!@options[:instrument_errors]
419
+ end
168
420
 
169
- # Provide support for raw values in the local cache strategy.
170
- module LocalCacheWithRaw # :nodoc:
421
+ # Make sure LocalCache is giving raw values, not `Entry`s, and
422
+ # respect `raw` option.
423
+ module LocalCacheEntryUnwrapAndRaw # :nodoc:
171
424
  protected
172
- def write_entry(key, entry, options) # :nodoc:
173
- retval = super
174
- if options[:raw] && local_cache && retval
175
- raw_entry = Entry.new(entry.value.to_s)
176
- raw_entry.expires_at = entry.expires_at
177
- local_cache.write_entry(key, raw_entry, options)
425
+ def read_entry(key, options)
426
+ retval = super(key, **options)
427
+ if retval.is_a? ActiveSupport::Cache::Entry
428
+ # Must have come from LocalStore, unwrap it
429
+ if options[:raw]
430
+ retval.value.to_s
431
+ else
432
+ retval.value
433
+ end
434
+ else
435
+ retval
178
436
  end
179
- retval
180
437
  end
181
438
  end
182
439
  end