activesupport 7.0.7.1 → 7.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (175) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +832 -283
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +4 -4
  5. data/lib/active_support/actionable_error.rb +3 -1
  6. data/lib/active_support/array_inquirer.rb +2 -0
  7. data/lib/active_support/backtrace_cleaner.rb +25 -5
  8. data/lib/active_support/benchmarkable.rb +1 -0
  9. data/lib/active_support/broadcast_logger.rb +242 -0
  10. data/lib/active_support/builder.rb +1 -1
  11. data/lib/active_support/cache/coder.rb +153 -0
  12. data/lib/active_support/cache/entry.rb +128 -0
  13. data/lib/active_support/cache/file_store.rb +36 -9
  14. data/lib/active_support/cache/mem_cache_store.rb +84 -68
  15. data/lib/active_support/cache/memory_store.rb +75 -21
  16. data/lib/active_support/cache/null_store.rb +6 -0
  17. data/lib/active_support/cache/redis_cache_store.rb +136 -131
  18. data/lib/active_support/cache/serializer_with_fallback.rb +175 -0
  19. data/lib/active_support/cache/strategy/local_cache.rb +20 -8
  20. data/lib/active_support/cache.rb +298 -246
  21. data/lib/active_support/callbacks.rb +44 -21
  22. data/lib/active_support/concern.rb +4 -2
  23. data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +42 -3
  24. data/lib/active_support/concurrency/null_lock.rb +13 -0
  25. data/lib/active_support/configurable.rb +10 -0
  26. data/lib/active_support/core_ext/array/conversions.rb +2 -1
  27. data/lib/active_support/core_ext/array.rb +0 -1
  28. data/lib/active_support/core_ext/class/subclasses.rb +13 -10
  29. data/lib/active_support/core_ext/date/conversions.rb +1 -0
  30. data/lib/active_support/core_ext/date.rb +0 -1
  31. data/lib/active_support/core_ext/date_and_time/calculations.rb +10 -0
  32. data/lib/active_support/core_ext/date_time/conversions.rb +6 -2
  33. data/lib/active_support/core_ext/date_time.rb +0 -1
  34. data/lib/active_support/core_ext/digest/uuid.rb +1 -10
  35. data/lib/active_support/core_ext/enumerable.rb +3 -75
  36. data/lib/active_support/core_ext/erb/util.rb +196 -0
  37. data/lib/active_support/core_ext/hash/conversions.rb +1 -1
  38. data/lib/active_support/core_ext/hash/deep_merge.rb +22 -14
  39. data/lib/active_support/core_ext/module/attribute_accessors.rb +6 -0
  40. data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +34 -16
  41. data/lib/active_support/core_ext/module/delegation.rb +40 -11
  42. data/lib/active_support/core_ext/module/deprecation.rb +15 -12
  43. data/lib/active_support/core_ext/module/introspection.rb +0 -1
  44. data/lib/active_support/core_ext/numeric/bytes.rb +9 -0
  45. data/lib/active_support/core_ext/numeric/conversions.rb +2 -0
  46. data/lib/active_support/core_ext/numeric.rb +0 -1
  47. data/lib/active_support/core_ext/object/deep_dup.rb +16 -0
  48. data/lib/active_support/core_ext/object/inclusion.rb +13 -5
  49. data/lib/active_support/core_ext/object/instance_variables.rb +22 -12
  50. data/lib/active_support/core_ext/object/json.rb +11 -3
  51. data/lib/active_support/core_ext/object/with.rb +44 -0
  52. data/lib/active_support/core_ext/object/with_options.rb +3 -3
  53. data/lib/active_support/core_ext/object.rb +1 -0
  54. data/lib/active_support/core_ext/pathname/blank.rb +16 -0
  55. data/lib/active_support/core_ext/pathname/existence.rb +2 -0
  56. data/lib/active_support/core_ext/pathname.rb +1 -0
  57. data/lib/active_support/core_ext/range/conversions.rb +28 -7
  58. data/lib/active_support/core_ext/range/overlap.rb +40 -0
  59. data/lib/active_support/core_ext/range.rb +1 -2
  60. data/lib/active_support/core_ext/securerandom.rb +24 -12
  61. data/lib/active_support/core_ext/string/filters.rb +20 -14
  62. data/lib/active_support/core_ext/string/inflections.rb +16 -5
  63. data/lib/active_support/core_ext/string/output_safety.rb +38 -174
  64. data/lib/active_support/core_ext/thread/backtrace/location.rb +12 -0
  65. data/lib/active_support/core_ext/time/calculations.rb +18 -2
  66. data/lib/active_support/core_ext/time/conversions.rb +2 -2
  67. data/lib/active_support/core_ext/time/zones.rb +4 -4
  68. data/lib/active_support/core_ext/time.rb +0 -1
  69. data/lib/active_support/current_attributes.rb +15 -6
  70. data/lib/active_support/deep_mergeable.rb +53 -0
  71. data/lib/active_support/dependencies/autoload.rb +17 -12
  72. data/lib/active_support/deprecation/behaviors.rb +55 -34
  73. data/lib/active_support/deprecation/constant_accessor.rb +5 -4
  74. data/lib/active_support/deprecation/deprecators.rb +104 -0
  75. data/lib/active_support/deprecation/disallowed.rb +3 -5
  76. data/lib/active_support/deprecation/instance_delegator.rb +31 -4
  77. data/lib/active_support/deprecation/method_wrappers.rb +6 -23
  78. data/lib/active_support/deprecation/proxy_wrappers.rb +37 -22
  79. data/lib/active_support/deprecation/reporting.rb +35 -21
  80. data/lib/active_support/deprecation.rb +32 -5
  81. data/lib/active_support/deprecator.rb +7 -0
  82. data/lib/active_support/descendants_tracker.rb +104 -132
  83. data/lib/active_support/duration/iso8601_serializer.rb +0 -2
  84. data/lib/active_support/duration.rb +2 -1
  85. data/lib/active_support/encrypted_configuration.rb +30 -9
  86. data/lib/active_support/encrypted_file.rb +8 -3
  87. data/lib/active_support/environment_inquirer.rb +22 -2
  88. data/lib/active_support/error_reporter/test_helper.rb +15 -0
  89. data/lib/active_support/error_reporter.rb +121 -35
  90. data/lib/active_support/execution_wrapper.rb +4 -4
  91. data/lib/active_support/file_update_checker.rb +4 -2
  92. data/lib/active_support/fork_tracker.rb +10 -2
  93. data/lib/active_support/gem_version.rb +4 -4
  94. data/lib/active_support/gzip.rb +2 -0
  95. data/lib/active_support/hash_with_indifferent_access.rb +35 -17
  96. data/lib/active_support/i18n.rb +1 -1
  97. data/lib/active_support/i18n_railtie.rb +20 -13
  98. data/lib/active_support/inflector/inflections.rb +2 -0
  99. data/lib/active_support/inflector/methods.rb +22 -10
  100. data/lib/active_support/inflector/transliterate.rb +3 -1
  101. data/lib/active_support/isolated_execution_state.rb +26 -22
  102. data/lib/active_support/json/decoding.rb +2 -1
  103. data/lib/active_support/json/encoding.rb +25 -43
  104. data/lib/active_support/key_generator.rb +9 -1
  105. data/lib/active_support/lazy_load_hooks.rb +6 -4
  106. data/lib/active_support/locale/en.yml +2 -0
  107. data/lib/active_support/log_subscriber.rb +78 -33
  108. data/lib/active_support/logger.rb +9 -60
  109. data/lib/active_support/logger_thread_safe_level.rb +10 -24
  110. data/lib/active_support/message_encryptor.rb +197 -53
  111. data/lib/active_support/message_encryptors.rb +141 -0
  112. data/lib/active_support/message_pack/cache_serializer.rb +23 -0
  113. data/lib/active_support/message_pack/extensions.rb +292 -0
  114. data/lib/active_support/message_pack/serializer.rb +63 -0
  115. data/lib/active_support/message_pack.rb +50 -0
  116. data/lib/active_support/message_verifier.rb +212 -93
  117. data/lib/active_support/message_verifiers.rb +135 -0
  118. data/lib/active_support/messages/codec.rb +65 -0
  119. data/lib/active_support/messages/metadata.rb +111 -45
  120. data/lib/active_support/messages/rotation_coordinator.rb +93 -0
  121. data/lib/active_support/messages/rotator.rb +34 -32
  122. data/lib/active_support/messages/serializer_with_fallback.rb +158 -0
  123. data/lib/active_support/multibyte/chars.rb +2 -0
  124. data/lib/active_support/multibyte/unicode.rb +9 -37
  125. data/lib/active_support/notifications/fanout.rb +239 -81
  126. data/lib/active_support/notifications/instrumenter.rb +77 -20
  127. data/lib/active_support/notifications.rb +1 -1
  128. data/lib/active_support/number_helper/number_converter.rb +14 -5
  129. data/lib/active_support/number_helper/number_to_currency_converter.rb +6 -6
  130. data/lib/active_support/number_helper/number_to_human_size_converter.rb +1 -1
  131. data/lib/active_support/number_helper/number_to_phone_converter.rb +1 -0
  132. data/lib/active_support/ordered_hash.rb +3 -3
  133. data/lib/active_support/ordered_options.rb +14 -0
  134. data/lib/active_support/parameter_filter.rb +84 -69
  135. data/lib/active_support/proxy_object.rb +2 -0
  136. data/lib/active_support/railtie.rb +33 -21
  137. data/lib/active_support/reloader.rb +12 -4
  138. data/lib/active_support/rescuable.rb +2 -0
  139. data/lib/active_support/secure_compare_rotator.rb +16 -9
  140. data/lib/active_support/string_inquirer.rb +3 -1
  141. data/lib/active_support/subscriber.rb +9 -27
  142. data/lib/active_support/syntax_error_proxy.rb +49 -0
  143. data/lib/active_support/tagged_logging.rb +60 -24
  144. data/lib/active_support/test_case.rb +153 -6
  145. data/lib/active_support/testing/assertions.rb +25 -9
  146. data/lib/active_support/testing/autorun.rb +0 -2
  147. data/lib/active_support/testing/constant_stubbing.rb +32 -0
  148. data/lib/active_support/testing/deprecation.rb +25 -25
  149. data/lib/active_support/testing/error_reporter_assertions.rb +107 -0
  150. data/lib/active_support/testing/isolation.rb +1 -1
  151. data/lib/active_support/testing/method_call_assertions.rb +21 -8
  152. data/lib/active_support/testing/parallelize_executor.rb +8 -3
  153. data/lib/active_support/testing/stream.rb +1 -1
  154. data/lib/active_support/testing/strict_warnings.rb +38 -0
  155. data/lib/active_support/testing/time_helpers.rb +32 -14
  156. data/lib/active_support/time_with_zone.rb +6 -42
  157. data/lib/active_support/values/time_zone.rb +9 -7
  158. data/lib/active_support/version.rb +1 -1
  159. data/lib/active_support/xml_mini/jdom.rb +3 -10
  160. data/lib/active_support/xml_mini/nokogiri.rb +1 -1
  161. data/lib/active_support/xml_mini/nokogirisax.rb +1 -1
  162. data/lib/active_support/xml_mini/rexml.rb +1 -1
  163. data/lib/active_support/xml_mini.rb +2 -2
  164. data/lib/active_support.rb +14 -3
  165. metadata +103 -16
  166. data/lib/active_support/core_ext/array/deprecated_conversions.rb +0 -25
  167. data/lib/active_support/core_ext/date/deprecated_conversions.rb +0 -37
  168. data/lib/active_support/core_ext/date_time/deprecated_conversions.rb +0 -33
  169. data/lib/active_support/core_ext/numeric/deprecated_conversions.rb +0 -60
  170. data/lib/active_support/core_ext/range/deprecated_conversions.rb +0 -33
  171. data/lib/active_support/core_ext/range/include_time_with_zone.rb +0 -5
  172. data/lib/active_support/core_ext/range/overlaps.rb +0 -10
  173. data/lib/active_support/core_ext/time/deprecated_conversions.rb +0 -33
  174. data/lib/active_support/core_ext/uri.rb +0 -5
  175. data/lib/active_support/per_thread_registry.rb +0 -65
@@ -2,14 +2,15 @@
2
2
 
3
3
  require "zlib"
4
4
  require "active_support/core_ext/array/extract_options"
5
- require "active_support/core_ext/array/wrap"
6
5
  require "active_support/core_ext/enumerable"
7
6
  require "active_support/core_ext/module/attribute_accessors"
8
7
  require "active_support/core_ext/numeric/bytes"
9
- require "active_support/core_ext/numeric/time"
10
8
  require "active_support/core_ext/object/to_param"
11
9
  require "active_support/core_ext/object/try"
12
10
  require "active_support/core_ext/string/inflections"
11
+ require_relative "cache/coder"
12
+ require_relative "cache/entry"
13
+ require_relative "cache/serializer_with_fallback"
13
14
 
14
15
  module ActiveSupport
15
16
  # See ActiveSupport::Cache::Store for documentation.
@@ -22,15 +23,31 @@ module ActiveSupport
22
23
 
23
24
  # These options mean something to all cache implementations. Individual cache
24
25
  # implementations may support additional options.
25
- UNIVERSAL_OPTIONS = [:namespace, :compress, :compress_threshold, :expires_in, :expire_in, :expired_in, :race_condition_ttl, :coder, :skip_nil]
26
-
27
- DEFAULT_COMPRESS_LIMIT = 1.kilobyte
26
+ UNIVERSAL_OPTIONS = [
27
+ :coder,
28
+ :compress,
29
+ :compress_threshold,
30
+ :compressor,
31
+ :expire_in,
32
+ :expired_in,
33
+ :expires_in,
34
+ :namespace,
35
+ :race_condition_ttl,
36
+ :serializer,
37
+ :skip_nil,
38
+ ]
28
39
 
29
40
  # Mapping of canonical option names to aliases that a store will recognize.
30
41
  OPTION_ALIASES = {
31
42
  expires_in: [:expire_in, :expired_in]
32
43
  }.freeze
33
44
 
45
+ DEFAULT_COMPRESS_LIMIT = 1.kilobyte
46
+
47
+ # Raised by coders when the cache entry can't be deserialized.
48
+ # This error is treated as a cache miss.
49
+ DeserializationError = Class.new(StandardError)
50
+
34
51
  module Strategy
35
52
  autoload :LocalCache, "active_support/cache/strategy/local_cache"
36
53
  end
@@ -132,6 +149,8 @@ module ActiveSupport
132
149
  end
133
150
  end
134
151
 
152
+ # = Active Support \Cache \Store
153
+ #
135
154
  # An abstract cache store class. There are multiple cache store
136
155
  # implementations, each having its own additional features. See the classes
137
156
  # under the ActiveSupport::Cache module, e.g.
@@ -174,24 +193,55 @@ module ActiveSupport
174
193
  #
175
194
  class Store
176
195
  cattr_accessor :logger, instance_writer: true
196
+ cattr_accessor :raise_on_invalid_cache_expiration_time, default: false
177
197
 
178
198
  attr_reader :silence, :options
179
199
  alias :silence? :silence
180
200
 
181
201
  class << self
182
202
  private
203
+ DEFAULT_POOL_OPTIONS = { size: 5, timeout: 5 }.freeze
204
+ private_constant :DEFAULT_POOL_OPTIONS
205
+
183
206
  def retrieve_pool_options(options)
184
- {}.tap do |pool_options|
185
- pool_options[:size] = options.delete(:pool_size) if options[:pool_size]
186
- pool_options[:timeout] = options.delete(:pool_timeout) if options[:pool_timeout]
207
+ if options.key?(:pool)
208
+ pool_options = options.delete(:pool)
209
+ elsif options.key?(:pool_size) || options.key?(:pool_timeout)
210
+ pool_options = {}
211
+
212
+ if options.key?(:pool_size)
213
+ ActiveSupport.deprecator.warn(<<~MSG)
214
+ Using :pool_size is deprecated and will be removed in Rails 7.2.
215
+ Use `pool: { size: #{options[:pool_size].inspect} }` instead.
216
+ MSG
217
+ pool_options[:size] = options.delete(:pool_size)
218
+ end
219
+
220
+ if options.key?(:pool_timeout)
221
+ ActiveSupport.deprecator.warn(<<~MSG)
222
+ Using :pool_timeout is deprecated and will be removed in Rails 7.2.
223
+ Use `pool: { timeout: #{options[:pool_timeout].inspect} }` instead.
224
+ MSG
225
+ pool_options[:timeout] = options.delete(:pool_timeout)
226
+ end
227
+ else
228
+ pool_options = true
187
229
  end
188
- end
189
230
 
190
- def ensure_connection_pool_added!
191
- require "connection_pool"
192
- rescue LoadError => e
193
- $stderr.puts "You don't have connection_pool installed in your application. Please add it to your Gemfile and run bundle install"
194
- raise e
231
+ case pool_options
232
+ when false, nil
233
+ return false
234
+ when true
235
+ pool_options = DEFAULT_POOL_OPTIONS
236
+ when Hash
237
+ pool_options[:size] = Integer(pool_options[:size]) if pool_options.key?(:size)
238
+ pool_options[:timeout] = Float(pool_options[:timeout]) if pool_options.key?(:timeout)
239
+ pool_options = DEFAULT_POOL_OPTIONS.merge(pool_options)
240
+ else
241
+ raise TypeError, "Invalid :pool argument, expected Hash, got: #{pool_options.inspect}"
242
+ end
243
+
244
+ pool_options unless pool_options.empty?
195
245
  end
196
246
  end
197
247
 
@@ -199,21 +249,90 @@ module ActiveSupport
199
249
  #
200
250
  # ==== Options
201
251
  #
202
- # * +:namespace+ - Sets the namespace for the cache. This option is
203
- # especially useful if your application shares a cache with other
204
- # applications.
205
- # * +:coder+ - Replaces the default cache entry serialization mechanism
206
- # with a custom one. The +coder+ must respond to +dump+ and +load+.
207
- # Using a custom coder disables automatic compression.
252
+ # [+:namespace+]
253
+ # Sets the namespace for the cache. This option is especially useful if
254
+ # your application shares a cache with other applications.
255
+ #
256
+ # [+:serializer+]
257
+ # The serializer for cached values. Must respond to +dump+ and +load+.
258
+ #
259
+ # The default serializer depends on the cache format version (set via
260
+ # +config.active_support.cache_format_version+ when using Rails). The
261
+ # default serializer for each format version includes a fallback
262
+ # mechanism to deserialize values from any format version. This behavior
263
+ # makes it easy to migrate between format versions without invalidating
264
+ # the entire cache.
265
+ #
266
+ # You can also specify <tt>serializer: :message_pack</tt> to use a
267
+ # preconfigured serializer based on ActiveSupport::MessagePack. The
268
+ # +:message_pack+ serializer includes the same deserialization fallback
269
+ # mechanism, allowing easy migration from (or to) the default
270
+ # serializer. The +:message_pack+ serializer may improve performance,
271
+ # but it requires the +msgpack+ gem.
272
+ #
273
+ # [+:compressor+]
274
+ # The compressor for serialized cache values. Must respond to +deflate+
275
+ # and +inflate+.
276
+ #
277
+ # The default compressor is +Zlib+. To define a new custom compressor
278
+ # that also decompresses old cache entries, you can check compressed
279
+ # values for Zlib's <tt>"\x78"</tt> signature:
280
+ #
281
+ # module MyCompressor
282
+ # def self.deflate(dumped)
283
+ # # compression logic... (make sure result does not start with "\x78"!)
284
+ # end
285
+ #
286
+ # def self.inflate(compressed)
287
+ # if compressed.start_with?("\x78")
288
+ # Zlib.inflate(compressed)
289
+ # else
290
+ # # decompression logic...
291
+ # end
292
+ # end
293
+ # end
294
+ #
295
+ # ActiveSupport::Cache.lookup_store(:redis_cache_store, compressor: MyCompressor)
296
+ #
297
+ # [+:coder+]
298
+ # The coder for serializing and (optionally) compressing cache entries.
299
+ # Must respond to +dump+ and +load+.
300
+ #
301
+ # The default coder composes the serializer and compressor, and includes
302
+ # some performance optimizations. If you only need to override the
303
+ # serializer or compressor, you should specify the +:serializer+ or
304
+ # +:compressor+ options instead.
305
+ #
306
+ # If the store can handle cache entries directly, you may also specify
307
+ # <tt>coder: nil</tt> to omit the serializer, compressor, and coder. For
308
+ # example, if you are using ActiveSupport::Cache::MemoryStore and can
309
+ # guarantee that cache values will not be mutated, you can specify
310
+ # <tt>coder: nil</tt> to avoid the overhead of safeguarding against
311
+ # mutation.
312
+ #
313
+ # The +:coder+ option is mutally exclusive with the +:serializer+ and
314
+ # +:compressor+ options. Specifying them together will raise an
315
+ # +ArgumentError+.
208
316
  #
209
317
  # Any other specified options are treated as default options for the
210
318
  # relevant cache operations, such as #read, #write, and #fetch.
211
319
  def initialize(options = nil)
212
- @options = options ? normalize_options(options) : {}
320
+ @options = options ? validate_options(normalize_options(options)) : {}
321
+
213
322
  @options[:compress] = true unless @options.key?(:compress)
214
- @options[:compress_threshold] = DEFAULT_COMPRESS_LIMIT unless @options.key?(:compress_threshold)
323
+ @options[:compress_threshold] ||= DEFAULT_COMPRESS_LIMIT
324
+
325
+ @coder = @options.delete(:coder) do
326
+ legacy_serializer = Cache.format_version < 7.1 && !@options[:serializer]
327
+ serializer = @options.delete(:serializer) || default_serializer
328
+ serializer = Cache::SerializerWithFallback[serializer] if serializer.is_a?(Symbol)
329
+ compressor = @options.delete(:compressor) { Zlib }
330
+
331
+ Cache::Coder.new(serializer, compressor, legacy_serializer: legacy_serializer)
332
+ end
333
+
334
+ @coder ||= Cache::SerializerWithFallback[:passthrough]
215
335
 
216
- @coder = @options.delete(:coder) { default_coder } || NullCoder
217
336
  @coder_supports_compression = @coder.respond_to?(:dump_compressed)
218
337
  end
219
338
 
@@ -318,18 +437,32 @@ module ActiveSupport
318
437
  # val_1 # => "new value 1"
319
438
  # val_2 # => "original value"
320
439
  #
440
+ # ==== Dynamic Options
441
+ #
442
+ # In some cases it may be necessary to dynamically compute options based
443
+ # on the cached value. To support this, an ActiveSupport::Cache::WriteOptions
444
+ # instance is passed as the second argument to the block. For example:
445
+ #
446
+ # cache.fetch("authentication-token:#{user.id}") do |key, options|
447
+ # token = authenticate_to_service
448
+ # options.expires_at = token.expires_at
449
+ # token
450
+ # end
451
+ #
321
452
  def fetch(name, options = nil, &block)
322
453
  if block_given?
323
454
  options = merged_options(options)
324
455
  key = normalize_key(name, options)
325
456
 
326
457
  entry = nil
327
- instrument(:read, name, options) do |payload|
328
- cached_entry = read_entry(key, **options, event: payload) unless options[:force]
329
- entry = handle_expired_entry(cached_entry, key, options)
330
- entry = nil if entry && entry.mismatched?(normalize_version(name, options))
331
- payload[:super_operation] = :fetch if payload
332
- payload[:hit] = !!entry if payload
458
+ unless options[:force]
459
+ instrument(:read, name, options) do |payload|
460
+ cached_entry = read_entry(key, **options, event: payload)
461
+ entry = handle_expired_entry(cached_entry, key, options)
462
+ entry = nil if entry && entry.mismatched?(normalize_version(name, options))
463
+ payload[:super_operation] = :fetch if payload
464
+ payload[:hit] = !!entry if payload
465
+ end
333
466
  end
334
467
 
335
468
  if entry
@@ -354,6 +487,7 @@ module ActiveSupport
354
487
  #
355
488
  # ==== Options
356
489
  #
490
+ # * +:namespace+ - Replace the store namespace for this call.
357
491
  # * +:version+ - Specifies a version for the cache entry. If the cached
358
492
  # version does not match the requested version, the read will be treated
359
493
  # as a cache miss. This feature is used to support recyclable cache keys.
@@ -393,10 +527,12 @@ module ActiveSupport
393
527
  #
394
528
  # Returns a hash mapping the names provided to the values found.
395
529
  def read_multi(*names)
530
+ return {} if names.empty?
531
+
396
532
  options = names.extract_options!
397
533
  options = merged_options(options)
398
534
 
399
- instrument :read_multi, names, options do |payload|
535
+ instrument_multi :read_multi, names, options do |payload|
400
536
  read_multi_entries(names, **options, event: payload).tap do |results|
401
537
  payload[:hits] = results.keys
402
538
  end
@@ -405,9 +541,11 @@ module ActiveSupport
405
541
 
406
542
  # Cache Storage API to write multiple values at once.
407
543
  def write_multi(hash, options = nil)
544
+ return hash if hash.empty?
545
+
408
546
  options = merged_options(options)
409
547
 
410
- instrument :write_multi, hash, options do |payload|
548
+ instrument_multi :write_multi, hash, options do |payload|
411
549
  entries = hash.each_with_object({}) do |(name, value), memo|
412
550
  memo[normalize_key(name, options)] = Entry.new(value, **options.merge(version: normalize_version(name, options)))
413
551
  end
@@ -433,7 +571,8 @@ module ActiveSupport
433
571
  # # => { "bim" => "bam",
434
572
  # # "unknown_key" => "Fallback value for key: unknown_key" }
435
573
  #
436
- # Options are passed to the underlying cache implementation. For example:
574
+ # You may also specify additional options via the +options+ argument. See #fetch for details.
575
+ # Other options are passed to the underlying cache implementation. For example:
437
576
  #
438
577
  # cache.fetch_multi("fizz", expires_in: 5.seconds) do |key|
439
578
  # "buzz"
@@ -446,16 +585,23 @@ module ActiveSupport
446
585
  # # => nil
447
586
  def fetch_multi(*names)
448
587
  raise ArgumentError, "Missing block: `Cache#fetch_multi` requires a block." unless block_given?
588
+ return {} if names.empty?
449
589
 
450
590
  options = names.extract_options!
451
591
  options = merged_options(options)
452
592
 
453
- instrument :read_multi, names, options do |payload|
454
- reads = read_multi_entries(names, **options)
593
+ instrument_multi :read_multi, names, options do |payload|
594
+ if options[:force]
595
+ reads = {}
596
+ else
597
+ reads = read_multi_entries(names, **options)
598
+ end
599
+
455
600
  writes = {}
456
601
  ordered = names.index_with do |name|
457
602
  reads.fetch(name) { writes[name] = yield(name) }
458
603
  end
604
+ writes.compact! if options[:skip_nil]
459
605
 
460
606
  payload[:hits] = reads.keys
461
607
  payload[:super_operation] = :fetch_multi
@@ -508,7 +654,8 @@ module ActiveSupport
508
654
  end
509
655
  end
510
656
 
511
- # Deletes an entry in the cache. Returns +true+ if an entry is deleted.
657
+ # Deletes an entry in the cache. Returns +true+ if an entry is deleted
658
+ # and +false+ otherwise.
512
659
  #
513
660
  # Options are passed to the underlying cache implementation.
514
661
  def delete(name, options = nil)
@@ -519,14 +666,17 @@ module ActiveSupport
519
666
  end
520
667
  end
521
668
 
522
- # Deletes multiple entries in the cache.
669
+ # Deletes multiple entries in the cache. Returns the number of deleted
670
+ # entries.
523
671
  #
524
672
  # Options are passed to the underlying cache implementation.
525
673
  def delete_multi(names, options = nil)
674
+ return 0 if names.empty?
675
+
526
676
  options = merged_options(options)
527
677
  names.map! { |key| normalize_key(key, options) }
528
678
 
529
- instrument :delete_multi, names do
679
+ instrument_multi :delete_multi, names do
530
680
  delete_multi_entries(names, **options)
531
681
  end
532
682
  end
@@ -594,8 +744,23 @@ module ActiveSupport
594
744
  end
595
745
 
596
746
  private
597
- def default_coder
598
- Coders[Cache.format_version]
747
+ def default_serializer
748
+ case Cache.format_version
749
+ when 6.1
750
+ ActiveSupport.deprecator.warn <<~EOM
751
+ Support for `config.active_support.cache_format_version = 6.1` has been deprecated and will be removed in Rails 7.2.
752
+
753
+ Check the Rails upgrade guide at https://guides.rubyonrails.org/upgrading_ruby_on_rails.html#new-activesupport-cache-serialization-format
754
+ for more information on how to upgrade.
755
+ EOM
756
+ Cache::SerializerWithFallback[:marshal_6_1]
757
+ when 7.0
758
+ Cache::SerializerWithFallback[:marshal_7_0]
759
+ when 7.1
760
+ Cache::SerializerWithFallback[:marshal_7_1]
761
+ else
762
+ raise ArgumentError, "Unrecognized ActiveSupport::Cache.format_version: #{Cache.format_version.inspect}"
763
+ end
599
764
  end
600
765
 
601
766
  # Adds the namespace defined in the options to a pattern designed to
@@ -632,7 +797,7 @@ module ActiveSupport
632
797
  def serialize_entry(entry, **options)
633
798
  options = merged_options(options)
634
799
  if @coder_supports_compression && options[:compress]
635
- @coder.dump_compressed(entry, options[:compress_threshold] || DEFAULT_COMPRESS_LIMIT)
800
+ @coder.dump_compressed(entry, options[:compress_threshold])
636
801
  else
637
802
  @coder.dump(entry)
638
803
  end
@@ -640,6 +805,8 @@ module ActiveSupport
640
805
 
641
806
  def deserialize_entry(payload)
642
807
  payload.nil? ? nil : @coder.load(payload)
808
+ rescue DeserializationError
809
+ nil
643
810
  end
644
811
 
645
812
  # Reads multiple entries from the cache implementation. Subclasses MAY
@@ -685,6 +852,22 @@ module ActiveSupport
685
852
  def merged_options(call_options)
686
853
  if call_options
687
854
  call_options = normalize_options(call_options)
855
+ if call_options.key?(:expires_in) && call_options.key?(:expires_at)
856
+ raise ArgumentError, "Either :expires_in or :expires_at can be supplied, but not both"
857
+ end
858
+
859
+ expires_at = call_options.delete(:expires_at)
860
+ call_options[:expires_in] = (expires_at - Time.now) if expires_at
861
+
862
+ if call_options[:expires_in].is_a?(Time)
863
+ expires_in = call_options[:expires_in]
864
+ raise ArgumentError.new("expires_in parameter should not be a Time. Did you mean to use expires_at? Got: #{expires_in}")
865
+ end
866
+ if call_options[:expires_in]&.negative?
867
+ expires_in = call_options.delete(:expires_in)
868
+ handle_invalid_expires_in("Cache expiration time is invalid, cannot be negative: #{expires_in}")
869
+ end
870
+
688
871
  if options.empty?
689
872
  call_options
690
873
  else
@@ -695,6 +878,16 @@ module ActiveSupport
695
878
  end
696
879
  end
697
880
 
881
+ def handle_invalid_expires_in(message)
882
+ error = ArgumentError.new(message)
883
+ if ActiveSupport::Cache::Store.raise_on_invalid_cache_expiration_time
884
+ raise error
885
+ else
886
+ ActiveSupport.error_reporter&.report(error, handled: true, severity: :warning)
887
+ logger.error("#{error.class}: #{error.message}") if logger
888
+ end
889
+ end
890
+
698
891
  # Normalize aliased options to their canonical form
699
892
  def normalize_options(options)
700
893
  options = options.dup
@@ -707,10 +900,31 @@ module ActiveSupport
707
900
  options
708
901
  end
709
902
 
710
- # Expands and namespaces the cache key. May be overridden by
711
- # cache stores to do additional normalization.
903
+ def validate_options(options)
904
+ if options.key?(:coder) && options[:serializer]
905
+ raise ArgumentError, "Cannot specify :serializer and :coder options together"
906
+ end
907
+
908
+ if options.key?(:coder) && options[:compressor]
909
+ raise ArgumentError, "Cannot specify :compressor and :coder options together"
910
+ end
911
+
912
+ if Cache.format_version < 7.1 && !options[:serializer] && options[:compressor]
913
+ raise ArgumentError, "Cannot specify :compressor option when using" \
914
+ " default serializer and cache format version is < 7.1"
915
+ end
916
+
917
+ options
918
+ end
919
+
920
+ # Expands and namespaces the cache key.
921
+ # Raises an exception when the key is +nil+ or an empty string.
922
+ # May be overridden by cache stores to do additional normalization.
712
923
  def normalize_key(key, options = nil)
713
- namespace_key expanded_key(key), options
924
+ str_key = expanded_key(key)
925
+ raise(ArgumentError, "key cannot be blank") if !str_key || str_key.empty?
926
+
927
+ namespace_key str_key, options
714
928
  end
715
929
 
716
930
  # Prefix the key with a namespace string:
@@ -773,14 +987,33 @@ module ActiveSupport
773
987
  end
774
988
  end
775
989
 
776
- def instrument(operation, key, options = nil)
990
+ def instrument(operation, key, options = nil, &block)
991
+ _instrument(operation, key: key, options: options, &block)
992
+ end
993
+
994
+ def instrument_multi(operation, keys, options = nil, &block)
995
+ _instrument(operation, multi: true, key: keys, options: options, &block)
996
+ end
997
+
998
+ def _instrument(operation, multi: false, options: nil, **payload, &block)
777
999
  if logger && logger.debug? && !silence?
778
- logger.debug "Cache #{operation}: #{normalize_key(key, options)}#{options.blank? ? "" : " (#{options.inspect})"}"
1000
+ debug_key =
1001
+ if multi
1002
+ ": #{payload[:key].size} key(s) specified"
1003
+ elsif payload[:key]
1004
+ ": #{normalize_key(payload[:key], options)}"
1005
+ end
1006
+
1007
+ debug_options = " (#{options.inspect})" unless options.blank?
1008
+
1009
+ logger.debug "Cache #{operation}#{debug_key}#{debug_options}"
779
1010
  end
780
1011
 
781
- payload = { key: key, store: self.class.name }
1012
+ payload[:store] = self.class.name
782
1013
  payload.merge!(options) if options.is_a?(Hash)
783
- ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload) { yield(payload) }
1014
+ ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload) do
1015
+ block&.call(payload)
1016
+ end
784
1017
  end
785
1018
 
786
1019
  def handle_expired_entry(entry, key, options)
@@ -800,13 +1033,13 @@ module ActiveSupport
800
1033
  end
801
1034
 
802
1035
  def get_entry_value(entry, name, options)
803
- instrument(:fetch_hit, name, options) { }
1036
+ instrument(:fetch_hit, name, options)
804
1037
  entry.value
805
1038
  end
806
1039
 
807
1040
  def save_block_result_to_cache(name, options)
808
1041
  result = instrument(:generate, name, options) do
809
- yield(name)
1042
+ yield(name, WriteOptions.new(options))
810
1043
  end
811
1044
 
812
1045
  write(name, result, options) unless result.nil? && options[:skip_nil]
@@ -814,217 +1047,36 @@ module ActiveSupport
814
1047
  end
815
1048
  end
816
1049
 
817
- module NullCoder # :nodoc:
818
- extend self
819
-
820
- def dump(entry)
821
- entry
822
- end
823
-
824
- def dump_compressed(entry, threshold)
825
- entry.compressed(threshold)
826
- end
827
-
828
- def load(payload)
829
- payload
830
- end
831
- end
832
-
833
- module Coders # :nodoc:
834
- MARK_61 = "\x04\b".b.freeze # The one set by Marshal.
835
- MARK_70_UNCOMPRESSED = "\x00".b.freeze
836
- MARK_70_COMPRESSED = "\x01".b.freeze
837
-
838
- class << self
839
- def [](version)
840
- case version
841
- when 6.1
842
- Rails61Coder
843
- when 7.0
844
- Rails70Coder
845
- else
846
- raise ArgumentError, "Unknown ActiveSupport::Cache.format_version: #{Cache.format_version.inspect}"
847
- end
848
- end
849
- end
850
-
851
- module Loader
852
- extend self
853
-
854
- def load(payload)
855
- if !payload.is_a?(String)
856
- ActiveSupport::Cache::Store.logger&.warn %{Payload wasn't a string, was #{payload.class.name} - couldn't unmarshal, so returning nil."}
857
-
858
- return nil
859
- elsif payload.start_with?(MARK_70_UNCOMPRESSED)
860
- members = Marshal.load(payload.byteslice(1..-1))
861
- elsif payload.start_with?(MARK_70_COMPRESSED)
862
- members = Marshal.load(Zlib::Inflate.inflate(payload.byteslice(1..-1)))
863
- elsif payload.start_with?(MARK_61)
864
- return Marshal.load(payload)
865
- else
866
- ActiveSupport::Cache::Store.logger&.warn %{Invalid cache prefix: #{payload.byteslice(0).inspect}, expected "\\x00" or "\\x01"}
867
-
868
- return nil
869
- end
870
- Entry.unpack(members)
871
- end
872
- end
873
-
874
- module Rails61Coder
875
- include Loader
876
- extend self
877
-
878
- def dump(entry)
879
- Marshal.dump(entry)
880
- end
881
-
882
- def dump_compressed(entry, threshold)
883
- Marshal.dump(entry.compressed(threshold))
884
- end
885
- end
886
-
887
- module Rails70Coder
888
- include Loader
889
- extend self
890
-
891
- def dump(entry)
892
- MARK_70_UNCOMPRESSED + Marshal.dump(entry.pack)
893
- end
894
-
895
- def dump_compressed(entry, threshold)
896
- payload = Marshal.dump(entry.pack)
897
- if payload.bytesize >= threshold
898
- compressed_payload = Zlib::Deflate.deflate(payload)
899
- if compressed_payload.bytesize < payload.bytesize
900
- return MARK_70_COMPRESSED + compressed_payload
901
- end
902
- end
903
-
904
- MARK_70_UNCOMPRESSED + payload
905
- end
906
- end
907
- end
908
-
909
- # This class is used to represent cache entries. Cache entries have a value, an optional
910
- # expiration time, and an optional version. The expiration time is used to support the :race_condition_ttl option
911
- # on the cache. The version is used to support the :version option on the cache for rejecting
912
- # mismatches.
913
- #
914
- # Since cache entries in most instances will be serialized, the internals of this class are highly optimized
915
- # using short instance variable names that are lazily defined.
916
- class Entry # :nodoc:
917
- class << self
918
- def unpack(members)
919
- new(members[0], expires_at: members[1], version: members[2])
920
- end
1050
+ class WriteOptions
1051
+ def initialize(options) # :nodoc:
1052
+ @options = options
921
1053
  end
922
1054
 
923
- attr_reader :version
924
-
925
- # Creates a new cache entry for the specified value. Options supported are
926
- # +:compressed+, +:version+, +:expires_at+ and +:expires_in+.
927
- def initialize(value, compressed: false, version: nil, expires_in: nil, expires_at: nil, **)
928
- @value = value
929
- @version = version
930
- @created_at = 0.0
931
- @expires_in = expires_at&.to_f || expires_in && (expires_in.to_f + Time.now.to_f)
932
- @compressed = true if compressed
1055
+ def version
1056
+ @options[:version]
933
1057
  end
934
1058
 
935
- def value
936
- compressed? ? uncompress(@value) : @value
1059
+ def version=(version)
1060
+ @options[:version] = version
937
1061
  end
938
1062
 
939
- def mismatched?(version)
940
- @version && version && @version != version
1063
+ def expires_in
1064
+ @options[:expires_in]
941
1065
  end
942
1066
 
943
- # Checks if the entry is expired. The +expires_in+ parameter can override
944
- # the value set when the entry was created.
945
- def expired?
946
- @expires_in && @created_at + @expires_in <= Time.now.to_f
1067
+ def expires_in=(expires_in)
1068
+ @options.delete(:expires_at)
1069
+ @options[:expires_in] = expires_in
947
1070
  end
948
1071
 
949
1072
  def expires_at
950
- @expires_in ? @created_at + @expires_in : nil
951
- end
952
-
953
- def expires_at=(value)
954
- if value
955
- @expires_in = value.to_f - @created_at
956
- else
957
- @expires_in = nil
958
- end
959
- end
960
-
961
- # Returns the size of the cached value. This could be less than
962
- # <tt>value.bytesize</tt> if the data is compressed.
963
- def bytesize
964
- case value
965
- when NilClass
966
- 0
967
- when String
968
- @value.bytesize
969
- else
970
- @s ||= Marshal.dump(@value).bytesize
971
- end
972
- end
973
-
974
- def compressed? # :nodoc:
975
- defined?(@compressed)
976
- end
977
-
978
- def compressed(compress_threshold)
979
- return self if compressed?
980
-
981
- case @value
982
- when nil, true, false, Numeric
983
- uncompressed_size = 0
984
- when String
985
- uncompressed_size = @value.bytesize
986
- else
987
- serialized = Marshal.dump(@value)
988
- uncompressed_size = serialized.bytesize
989
- end
990
-
991
- if uncompressed_size >= compress_threshold
992
- serialized ||= Marshal.dump(@value)
993
- compressed = Zlib::Deflate.deflate(serialized)
994
-
995
- if compressed.bytesize < uncompressed_size
996
- return Entry.new(compressed, compressed: true, expires_at: expires_at, version: version)
997
- end
998
- end
999
- self
1000
- end
1001
-
1002
- def local?
1003
- false
1073
+ @options[:expires_at]
1004
1074
  end
1005
1075
 
1006
- # Duplicates the value in a class. This is used by cache implementations that don't natively
1007
- # serialize entries to protect against accidental cache modifications.
1008
- def dup_value!
1009
- if @value && !compressed? && !(@value.is_a?(Numeric) || @value == true || @value == false)
1010
- if @value.is_a?(String)
1011
- @value = @value.dup
1012
- else
1013
- @value = Marshal.load(Marshal.dump(@value))
1014
- end
1015
- end
1076
+ def expires_at=(expires_at)
1077
+ @options.delete(:expires_in)
1078
+ @options[:expires_at] = expires_at
1016
1079
  end
1017
-
1018
- def pack
1019
- members = [value, expires_at, version]
1020
- members.pop while !members.empty? && members.last.nil?
1021
- members
1022
- end
1023
-
1024
- private
1025
- def uncompress(value)
1026
- Marshal.load(Zlib::Inflate.inflate(value))
1027
- end
1028
1080
  end
1029
1081
  end
1030
1082
  end