activesupport 7.0.8 → 7.1.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (170) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +736 -293
  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/builder.rb +1 -1
  10. data/lib/active_support/cache/coder.rb +153 -0
  11. data/lib/active_support/cache/entry.rb +128 -0
  12. data/lib/active_support/cache/file_store.rb +36 -9
  13. data/lib/active_support/cache/mem_cache_store.rb +84 -68
  14. data/lib/active_support/cache/memory_store.rb +76 -24
  15. data/lib/active_support/cache/null_store.rb +6 -0
  16. data/lib/active_support/cache/redis_cache_store.rb +126 -131
  17. data/lib/active_support/cache/serializer_with_fallback.rb +175 -0
  18. data/lib/active_support/cache/strategy/local_cache.rb +20 -8
  19. data/lib/active_support/cache.rb +304 -246
  20. data/lib/active_support/callbacks.rb +38 -18
  21. data/lib/active_support/concern.rb +4 -2
  22. data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +42 -3
  23. data/lib/active_support/concurrency/null_lock.rb +13 -0
  24. data/lib/active_support/configurable.rb +10 -0
  25. data/lib/active_support/core_ext/array/conversions.rb +2 -1
  26. data/lib/active_support/core_ext/array.rb +0 -1
  27. data/lib/active_support/core_ext/class/subclasses.rb +13 -10
  28. data/lib/active_support/core_ext/date/conversions.rb +1 -0
  29. data/lib/active_support/core_ext/date.rb +0 -1
  30. data/lib/active_support/core_ext/date_and_time/calculations.rb +10 -0
  31. data/lib/active_support/core_ext/date_time/conversions.rb +6 -2
  32. data/lib/active_support/core_ext/date_time.rb +0 -1
  33. data/lib/active_support/core_ext/digest/uuid.rb +1 -10
  34. data/lib/active_support/core_ext/enumerable.rb +3 -75
  35. data/lib/active_support/core_ext/erb/util.rb +196 -0
  36. data/lib/active_support/core_ext/hash/conversions.rb +1 -1
  37. data/lib/active_support/core_ext/module/attribute_accessors.rb +6 -0
  38. data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +34 -16
  39. data/lib/active_support/core_ext/module/delegation.rb +40 -11
  40. data/lib/active_support/core_ext/module/deprecation.rb +15 -12
  41. data/lib/active_support/core_ext/module/introspection.rb +0 -1
  42. data/lib/active_support/core_ext/numeric/bytes.rb +9 -0
  43. data/lib/active_support/core_ext/numeric/conversions.rb +2 -0
  44. data/lib/active_support/core_ext/numeric.rb +0 -1
  45. data/lib/active_support/core_ext/object/deep_dup.rb +16 -0
  46. data/lib/active_support/core_ext/object/inclusion.rb +13 -5
  47. data/lib/active_support/core_ext/object/instance_variables.rb +22 -12
  48. data/lib/active_support/core_ext/object/json.rb +10 -2
  49. data/lib/active_support/core_ext/object/with.rb +44 -0
  50. data/lib/active_support/core_ext/object/with_options.rb +3 -3
  51. data/lib/active_support/core_ext/object.rb +1 -0
  52. data/lib/active_support/core_ext/pathname/blank.rb +16 -0
  53. data/lib/active_support/core_ext/pathname/existence.rb +2 -0
  54. data/lib/active_support/core_ext/pathname.rb +1 -0
  55. data/lib/active_support/core_ext/range/conversions.rb +28 -7
  56. data/lib/active_support/core_ext/range/{overlaps.rb → overlap.rb} +5 -3
  57. data/lib/active_support/core_ext/range.rb +1 -2
  58. data/lib/active_support/core_ext/securerandom.rb +24 -12
  59. data/lib/active_support/core_ext/string/filters.rb +20 -14
  60. data/lib/active_support/core_ext/string/inflections.rb +16 -5
  61. data/lib/active_support/core_ext/string/output_safety.rb +38 -174
  62. data/lib/active_support/core_ext/thread/backtrace/location.rb +12 -0
  63. data/lib/active_support/core_ext/time/calculations.rb +18 -2
  64. data/lib/active_support/core_ext/time/conversions.rb +2 -2
  65. data/lib/active_support/core_ext/time/zones.rb +4 -4
  66. data/lib/active_support/core_ext/time.rb +0 -1
  67. data/lib/active_support/current_attributes.rb +15 -6
  68. data/lib/active_support/dependencies/autoload.rb +17 -12
  69. data/lib/active_support/deprecation/behaviors.rb +53 -32
  70. data/lib/active_support/deprecation/constant_accessor.rb +5 -4
  71. data/lib/active_support/deprecation/deprecators.rb +104 -0
  72. data/lib/active_support/deprecation/disallowed.rb +3 -5
  73. data/lib/active_support/deprecation/instance_delegator.rb +31 -4
  74. data/lib/active_support/deprecation/method_wrappers.rb +6 -23
  75. data/lib/active_support/deprecation/proxy_wrappers.rb +37 -22
  76. data/lib/active_support/deprecation/reporting.rb +35 -21
  77. data/lib/active_support/deprecation.rb +32 -5
  78. data/lib/active_support/deprecator.rb +7 -0
  79. data/lib/active_support/descendants_tracker.rb +104 -132
  80. data/lib/active_support/duration/iso8601_serializer.rb +0 -2
  81. data/lib/active_support/duration.rb +2 -1
  82. data/lib/active_support/encrypted_configuration.rb +30 -9
  83. data/lib/active_support/encrypted_file.rb +8 -3
  84. data/lib/active_support/environment_inquirer.rb +22 -2
  85. data/lib/active_support/error_reporter/test_helper.rb +15 -0
  86. data/lib/active_support/error_reporter.rb +121 -35
  87. data/lib/active_support/execution_wrapper.rb +4 -4
  88. data/lib/active_support/file_update_checker.rb +4 -2
  89. data/lib/active_support/fork_tracker.rb +10 -2
  90. data/lib/active_support/gem_version.rb +4 -4
  91. data/lib/active_support/gzip.rb +2 -0
  92. data/lib/active_support/hash_with_indifferent_access.rb +35 -17
  93. data/lib/active_support/i18n.rb +1 -1
  94. data/lib/active_support/i18n_railtie.rb +20 -13
  95. data/lib/active_support/inflector/inflections.rb +2 -0
  96. data/lib/active_support/inflector/methods.rb +22 -10
  97. data/lib/active_support/inflector/transliterate.rb +3 -1
  98. data/lib/active_support/isolated_execution_state.rb +26 -22
  99. data/lib/active_support/json/decoding.rb +2 -1
  100. data/lib/active_support/json/encoding.rb +25 -43
  101. data/lib/active_support/key_generator.rb +9 -1
  102. data/lib/active_support/lazy_load_hooks.rb +6 -4
  103. data/lib/active_support/locale/en.yml +2 -0
  104. data/lib/active_support/log_subscriber.rb +78 -33
  105. data/lib/active_support/logger.rb +1 -1
  106. data/lib/active_support/logger_thread_safe_level.rb +9 -21
  107. data/lib/active_support/message_encryptor.rb +197 -53
  108. data/lib/active_support/message_encryptors.rb +140 -0
  109. data/lib/active_support/message_pack/cache_serializer.rb +23 -0
  110. data/lib/active_support/message_pack/extensions.rb +292 -0
  111. data/lib/active_support/message_pack/serializer.rb +63 -0
  112. data/lib/active_support/message_pack.rb +50 -0
  113. data/lib/active_support/message_verifier.rb +212 -93
  114. data/lib/active_support/message_verifiers.rb +134 -0
  115. data/lib/active_support/messages/codec.rb +65 -0
  116. data/lib/active_support/messages/metadata.rb +111 -45
  117. data/lib/active_support/messages/rotation_coordinator.rb +93 -0
  118. data/lib/active_support/messages/rotator.rb +34 -32
  119. data/lib/active_support/messages/serializer_with_fallback.rb +158 -0
  120. data/lib/active_support/multibyte/chars.rb +2 -0
  121. data/lib/active_support/multibyte/unicode.rb +9 -37
  122. data/lib/active_support/notifications/fanout.rb +239 -81
  123. data/lib/active_support/notifications/instrumenter.rb +71 -14
  124. data/lib/active_support/notifications.rb +1 -1
  125. data/lib/active_support/number_helper/number_converter.rb +2 -2
  126. data/lib/active_support/number_helper/number_to_human_size_converter.rb +1 -1
  127. data/lib/active_support/number_helper/number_to_phone_converter.rb +1 -0
  128. data/lib/active_support/ordered_hash.rb +3 -3
  129. data/lib/active_support/ordered_options.rb +14 -0
  130. data/lib/active_support/parameter_filter.rb +84 -69
  131. data/lib/active_support/proxy_object.rb +2 -0
  132. data/lib/active_support/railtie.rb +33 -21
  133. data/lib/active_support/reloader.rb +12 -4
  134. data/lib/active_support/rescuable.rb +2 -0
  135. data/lib/active_support/secure_compare_rotator.rb +16 -9
  136. data/lib/active_support/string_inquirer.rb +3 -1
  137. data/lib/active_support/subscriber.rb +9 -27
  138. data/lib/active_support/syntax_error_proxy.rb +49 -0
  139. data/lib/active_support/tagged_logging.rb +60 -24
  140. data/lib/active_support/test_case.rb +153 -6
  141. data/lib/active_support/testing/assertions.rb +25 -9
  142. data/lib/active_support/testing/autorun.rb +0 -2
  143. data/lib/active_support/testing/constant_stubbing.rb +32 -0
  144. data/lib/active_support/testing/deprecation.rb +25 -25
  145. data/lib/active_support/testing/error_reporter_assertions.rb +108 -0
  146. data/lib/active_support/testing/isolation.rb +1 -1
  147. data/lib/active_support/testing/method_call_assertions.rb +21 -8
  148. data/lib/active_support/testing/parallelize_executor.rb +8 -3
  149. data/lib/active_support/testing/stream.rb +1 -1
  150. data/lib/active_support/testing/strict_warnings.rb +38 -0
  151. data/lib/active_support/testing/time_helpers.rb +32 -14
  152. data/lib/active_support/time_with_zone.rb +4 -14
  153. data/lib/active_support/values/time_zone.rb +9 -7
  154. data/lib/active_support/version.rb +1 -1
  155. data/lib/active_support/xml_mini/jdom.rb +3 -10
  156. data/lib/active_support/xml_mini/nokogiri.rb +1 -1
  157. data/lib/active_support/xml_mini/nokogirisax.rb +1 -1
  158. data/lib/active_support/xml_mini/rexml.rb +1 -1
  159. data/lib/active_support/xml_mini.rb +2 -2
  160. data/lib/active_support.rb +13 -3
  161. metadata +102 -17
  162. data/lib/active_support/core_ext/array/deprecated_conversions.rb +0 -25
  163. data/lib/active_support/core_ext/date/deprecated_conversions.rb +0 -40
  164. data/lib/active_support/core_ext/date_time/deprecated_conversions.rb +0 -36
  165. data/lib/active_support/core_ext/numeric/deprecated_conversions.rb +0 -60
  166. data/lib/active_support/core_ext/range/deprecated_conversions.rb +0 -36
  167. data/lib/active_support/core_ext/range/include_time_with_zone.rb +0 -5
  168. data/lib/active_support/core_ext/time/deprecated_conversions.rb +0 -73
  169. data/lib/active_support/core_ext/uri.rb +0 -5
  170. 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,38 @@ 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 to dynamically compute options based
443
+ # on the cached value. For this purpose, a ActiveSupport::Cache::WriteOptions
444
+ # instance is passed as a second argument to the block
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
+ #
452
+ # Only some options can be set dynamically:
453
+ #
454
+ # - +:expires_in+
455
+ # - +:expires_at+
456
+ # - +:version+
457
+ #
321
458
  def fetch(name, options = nil, &block)
322
459
  if block_given?
323
460
  options = merged_options(options)
324
461
  key = normalize_key(name, options)
325
462
 
326
463
  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
464
+ unless options[:force]
465
+ instrument(:read, name, options) do |payload|
466
+ cached_entry = read_entry(key, **options, event: payload)
467
+ entry = handle_expired_entry(cached_entry, key, options)
468
+ entry = nil if entry && entry.mismatched?(normalize_version(name, options))
469
+ payload[:super_operation] = :fetch if payload
470
+ payload[:hit] = !!entry if payload
471
+ end
333
472
  end
334
473
 
335
474
  if entry
@@ -354,6 +493,7 @@ module ActiveSupport
354
493
  #
355
494
  # ==== Options
356
495
  #
496
+ # * +:namespace+ - Replace the store namespace for this call.
357
497
  # * +:version+ - Specifies a version for the cache entry. If the cached
358
498
  # version does not match the requested version, the read will be treated
359
499
  # as a cache miss. This feature is used to support recyclable cache keys.
@@ -393,10 +533,12 @@ module ActiveSupport
393
533
  #
394
534
  # Returns a hash mapping the names provided to the values found.
395
535
  def read_multi(*names)
536
+ return {} if names.empty?
537
+
396
538
  options = names.extract_options!
397
539
  options = merged_options(options)
398
540
 
399
- instrument :read_multi, names, options do |payload|
541
+ instrument_multi :read_multi, names, options do |payload|
400
542
  read_multi_entries(names, **options, event: payload).tap do |results|
401
543
  payload[:hits] = results.keys
402
544
  end
@@ -405,9 +547,11 @@ module ActiveSupport
405
547
 
406
548
  # Cache Storage API to write multiple values at once.
407
549
  def write_multi(hash, options = nil)
550
+ return hash if hash.empty?
551
+
408
552
  options = merged_options(options)
409
553
 
410
- instrument :write_multi, hash, options do |payload|
554
+ instrument_multi :write_multi, hash, options do |payload|
411
555
  entries = hash.each_with_object({}) do |(name, value), memo|
412
556
  memo[normalize_key(name, options)] = Entry.new(value, **options.merge(version: normalize_version(name, options)))
413
557
  end
@@ -433,7 +577,8 @@ module ActiveSupport
433
577
  # # => { "bim" => "bam",
434
578
  # # "unknown_key" => "Fallback value for key: unknown_key" }
435
579
  #
436
- # Options are passed to the underlying cache implementation. For example:
580
+ # You may also specify additional options via the +options+ argument. See #fetch for details.
581
+ # Other options are passed to the underlying cache implementation. For example:
437
582
  #
438
583
  # cache.fetch_multi("fizz", expires_in: 5.seconds) do |key|
439
584
  # "buzz"
@@ -446,16 +591,23 @@ module ActiveSupport
446
591
  # # => nil
447
592
  def fetch_multi(*names)
448
593
  raise ArgumentError, "Missing block: `Cache#fetch_multi` requires a block." unless block_given?
594
+ return {} if names.empty?
449
595
 
450
596
  options = names.extract_options!
451
597
  options = merged_options(options)
452
598
 
453
- instrument :read_multi, names, options do |payload|
454
- reads = read_multi_entries(names, **options)
599
+ instrument_multi :read_multi, names, options do |payload|
600
+ if options[:force]
601
+ reads = {}
602
+ else
603
+ reads = read_multi_entries(names, **options)
604
+ end
605
+
455
606
  writes = {}
456
607
  ordered = names.index_with do |name|
457
608
  reads.fetch(name) { writes[name] = yield(name) }
458
609
  end
610
+ writes.compact! if options[:skip_nil]
459
611
 
460
612
  payload[:hits] = reads.keys
461
613
  payload[:super_operation] = :fetch_multi
@@ -508,7 +660,8 @@ module ActiveSupport
508
660
  end
509
661
  end
510
662
 
511
- # Deletes an entry in the cache. Returns +true+ if an entry is deleted.
663
+ # Deletes an entry in the cache. Returns +true+ if an entry is deleted
664
+ # and +false+ otherwise.
512
665
  #
513
666
  # Options are passed to the underlying cache implementation.
514
667
  def delete(name, options = nil)
@@ -519,14 +672,17 @@ module ActiveSupport
519
672
  end
520
673
  end
521
674
 
522
- # Deletes multiple entries in the cache.
675
+ # Deletes multiple entries in the cache. Returns the number of deleted
676
+ # entries.
523
677
  #
524
678
  # Options are passed to the underlying cache implementation.
525
679
  def delete_multi(names, options = nil)
680
+ return 0 if names.empty?
681
+
526
682
  options = merged_options(options)
527
683
  names.map! { |key| normalize_key(key, options) }
528
684
 
529
- instrument :delete_multi, names do
685
+ instrument_multi :delete_multi, names do
530
686
  delete_multi_entries(names, **options)
531
687
  end
532
688
  end
@@ -594,8 +750,23 @@ module ActiveSupport
594
750
  end
595
751
 
596
752
  private
597
- def default_coder
598
- Coders[Cache.format_version]
753
+ def default_serializer
754
+ case Cache.format_version
755
+ when 6.1
756
+ ActiveSupport.deprecator.warn <<~EOM
757
+ Support for `config.active_support.cache_format_version = 6.1` has been deprecated and will be removed in Rails 7.2.
758
+
759
+ Check the Rails upgrade guide at https://guides.rubyonrails.org/upgrading_ruby_on_rails.html#new-activesupport-cache-serialization-format
760
+ for more information on how to upgrade.
761
+ EOM
762
+ Cache::SerializerWithFallback[:marshal_6_1]
763
+ when 7.0
764
+ Cache::SerializerWithFallback[:marshal_7_0]
765
+ when 7.1
766
+ Cache::SerializerWithFallback[:marshal_7_1]
767
+ else
768
+ raise ArgumentError, "Unrecognized ActiveSupport::Cache.format_version: #{Cache.format_version.inspect}"
769
+ end
599
770
  end
600
771
 
601
772
  # Adds the namespace defined in the options to a pattern designed to
@@ -632,7 +803,7 @@ module ActiveSupport
632
803
  def serialize_entry(entry, **options)
633
804
  options = merged_options(options)
634
805
  if @coder_supports_compression && options[:compress]
635
- @coder.dump_compressed(entry, options[:compress_threshold] || DEFAULT_COMPRESS_LIMIT)
806
+ @coder.dump_compressed(entry, options[:compress_threshold])
636
807
  else
637
808
  @coder.dump(entry)
638
809
  end
@@ -640,6 +811,8 @@ module ActiveSupport
640
811
 
641
812
  def deserialize_entry(payload)
642
813
  payload.nil? ? nil : @coder.load(payload)
814
+ rescue DeserializationError
815
+ nil
643
816
  end
644
817
 
645
818
  # Reads multiple entries from the cache implementation. Subclasses MAY
@@ -685,6 +858,22 @@ module ActiveSupport
685
858
  def merged_options(call_options)
686
859
  if call_options
687
860
  call_options = normalize_options(call_options)
861
+ if call_options.key?(:expires_in) && call_options.key?(:expires_at)
862
+ raise ArgumentError, "Either :expires_in or :expires_at can be supplied, but not both"
863
+ end
864
+
865
+ expires_at = call_options.delete(:expires_at)
866
+ call_options[:expires_in] = (expires_at - Time.now) if expires_at
867
+
868
+ if call_options[:expires_in].is_a?(Time)
869
+ expires_in = call_options[:expires_in]
870
+ raise ArgumentError.new("expires_in parameter should not be a Time. Did you mean to use expires_at? Got: #{expires_in}")
871
+ end
872
+ if call_options[:expires_in]&.negative?
873
+ expires_in = call_options.delete(:expires_in)
874
+ handle_invalid_expires_in("Cache expiration time is invalid, cannot be negative: #{expires_in}")
875
+ end
876
+
688
877
  if options.empty?
689
878
  call_options
690
879
  else
@@ -695,6 +884,16 @@ module ActiveSupport
695
884
  end
696
885
  end
697
886
 
887
+ def handle_invalid_expires_in(message)
888
+ error = ArgumentError.new(message)
889
+ if ActiveSupport::Cache::Store.raise_on_invalid_cache_expiration_time
890
+ raise error
891
+ else
892
+ ActiveSupport.error_reporter&.report(error, handled: true, severity: :warning)
893
+ logger.error("#{error.class}: #{error.message}") if logger
894
+ end
895
+ end
896
+
698
897
  # Normalize aliased options to their canonical form
699
898
  def normalize_options(options)
700
899
  options = options.dup
@@ -707,10 +906,31 @@ module ActiveSupport
707
906
  options
708
907
  end
709
908
 
710
- # Expands and namespaces the cache key. May be overridden by
711
- # cache stores to do additional normalization.
909
+ def validate_options(options)
910
+ if options.key?(:coder) && options[:serializer]
911
+ raise ArgumentError, "Cannot specify :serializer and :coder options together"
912
+ end
913
+
914
+ if options.key?(:coder) && options[:compressor]
915
+ raise ArgumentError, "Cannot specify :compressor and :coder options together"
916
+ end
917
+
918
+ if Cache.format_version < 7.1 && !options[:serializer] && options[:compressor]
919
+ raise ArgumentError, "Cannot specify :compressor option when using" \
920
+ " default serializer and cache format version is < 7.1"
921
+ end
922
+
923
+ options
924
+ end
925
+
926
+ # Expands and namespaces the cache key.
927
+ # Raises an exception when the key is +nil+ or an empty string.
928
+ # May be overridden by cache stores to do additional normalization.
712
929
  def normalize_key(key, options = nil)
713
- namespace_key expanded_key(key), options
930
+ str_key = expanded_key(key)
931
+ raise(ArgumentError, "key cannot be blank") if !str_key || str_key.empty?
932
+
933
+ namespace_key str_key, options
714
934
  end
715
935
 
716
936
  # Prefix the key with a namespace string:
@@ -773,14 +993,33 @@ module ActiveSupport
773
993
  end
774
994
  end
775
995
 
776
- def instrument(operation, key, options = nil)
996
+ def instrument(operation, key, options = nil, &block)
997
+ _instrument(operation, key: key, options: options, &block)
998
+ end
999
+
1000
+ def instrument_multi(operation, keys, options = nil, &block)
1001
+ _instrument(operation, multi: true, key: keys, options: options, &block)
1002
+ end
1003
+
1004
+ def _instrument(operation, multi: false, options: nil, **payload, &block)
777
1005
  if logger && logger.debug? && !silence?
778
- logger.debug "Cache #{operation}: #{normalize_key(key, options)}#{options.blank? ? "" : " (#{options.inspect})"}"
1006
+ debug_key =
1007
+ if multi
1008
+ ": #{payload[:key].size} key(s) specified"
1009
+ elsif payload[:key]
1010
+ ": #{normalize_key(payload[:key], options)}"
1011
+ end
1012
+
1013
+ debug_options = " (#{options.inspect})" unless options.blank?
1014
+
1015
+ logger.debug "Cache #{operation}#{debug_key}#{debug_options}"
779
1016
  end
780
1017
 
781
- payload = { key: key, store: self.class.name }
1018
+ payload[:store] = self.class.name
782
1019
  payload.merge!(options) if options.is_a?(Hash)
783
- ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload) { yield(payload) }
1020
+ ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload) do
1021
+ block&.call(payload)
1022
+ end
784
1023
  end
785
1024
 
786
1025
  def handle_expired_entry(entry, key, options)
@@ -800,13 +1039,13 @@ module ActiveSupport
800
1039
  end
801
1040
 
802
1041
  def get_entry_value(entry, name, options)
803
- instrument(:fetch_hit, name, options) { }
1042
+ instrument(:fetch_hit, name, options)
804
1043
  entry.value
805
1044
  end
806
1045
 
807
1046
  def save_block_result_to_cache(name, options)
808
1047
  result = instrument(:generate, name, options) do
809
- yield(name)
1048
+ yield(name, WriteOptions.new(options))
810
1049
  end
811
1050
 
812
1051
  write(name, result, options) unless result.nil? && options[:skip_nil]
@@ -814,217 +1053,36 @@ module ActiveSupport
814
1053
  end
815
1054
  end
816
1055
 
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
1056
+ class WriteOptions
1057
+ def initialize(options) # :nodoc:
1058
+ @options = options
921
1059
  end
922
1060
 
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
1061
+ def version
1062
+ @options[:version]
933
1063
  end
934
1064
 
935
- def value
936
- compressed? ? uncompress(@value) : @value
1065
+ def version=(version)
1066
+ @options[:version] = version
937
1067
  end
938
1068
 
939
- def mismatched?(version)
940
- @version && version && @version != version
1069
+ def expires_in
1070
+ @options[:expires_in]
941
1071
  end
942
1072
 
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
1073
+ def expires_in=(expires_in)
1074
+ @options.delete(:expires_at)
1075
+ @options[:expires_in] = expires_in
947
1076
  end
948
1077
 
949
1078
  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
1079
+ @options[:expires_at]
1004
1080
  end
1005
1081
 
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
1082
+ def expires_at=(expires_at)
1083
+ @options.delete(:expires_in)
1084
+ @options[:expires_at] = expires_at
1016
1085
  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
1086
  end
1029
1087
  end
1030
1088
  end