activesupport 7.0.8.7 → 7.1.0.beta1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +722 -314
  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/duplicable.rb +15 -24
  47. data/lib/active_support/core_ext/object/inclusion.rb +13 -5
  48. data/lib/active_support/core_ext/object/instance_variables.rb +22 -12
  49. data/lib/active_support/core_ext/object/json.rb +10 -2
  50. data/lib/active_support/core_ext/object/with.rb +44 -0
  51. data/lib/active_support/core_ext/object/with_options.rb +3 -3
  52. data/lib/active_support/core_ext/object.rb +1 -0
  53. data/lib/active_support/core_ext/pathname/blank.rb +16 -0
  54. data/lib/active_support/core_ext/pathname/existence.rb +2 -0
  55. data/lib/active_support/core_ext/pathname.rb +1 -0
  56. data/lib/active_support/core_ext/range/conversions.rb +28 -7
  57. data/lib/active_support/core_ext/range/{overlaps.rb → overlap.rb} +5 -3
  58. data/lib/active_support/core_ext/range.rb +1 -2
  59. data/lib/active_support/core_ext/securerandom.rb +24 -12
  60. data/lib/active_support/core_ext/string/filters.rb +20 -14
  61. data/lib/active_support/core_ext/string/inflections.rb +16 -5
  62. data/lib/active_support/core_ext/string/output_safety.rb +38 -174
  63. data/lib/active_support/core_ext/thread/backtrace/location.rb +12 -0
  64. data/lib/active_support/core_ext/time/calculations.rb +18 -2
  65. data/lib/active_support/core_ext/time/conversions.rb +2 -2
  66. data/lib/active_support/core_ext/time/zones.rb +4 -4
  67. data/lib/active_support/core_ext/time.rb +0 -1
  68. data/lib/active_support/current_attributes.rb +15 -6
  69. data/lib/active_support/dependencies/autoload.rb +17 -12
  70. data/lib/active_support/deprecation/behaviors.rb +53 -32
  71. data/lib/active_support/deprecation/constant_accessor.rb +5 -4
  72. data/lib/active_support/deprecation/deprecators.rb +104 -0
  73. data/lib/active_support/deprecation/disallowed.rb +3 -5
  74. data/lib/active_support/deprecation/instance_delegator.rb +31 -4
  75. data/lib/active_support/deprecation/method_wrappers.rb +6 -23
  76. data/lib/active_support/deprecation/proxy_wrappers.rb +37 -22
  77. data/lib/active_support/deprecation/reporting.rb +35 -21
  78. data/lib/active_support/deprecation.rb +32 -5
  79. data/lib/active_support/deprecator.rb +7 -0
  80. data/lib/active_support/descendants_tracker.rb +104 -132
  81. data/lib/active_support/duration/iso8601_serializer.rb +0 -2
  82. data/lib/active_support/duration.rb +2 -1
  83. data/lib/active_support/encrypted_configuration.rb +30 -9
  84. data/lib/active_support/encrypted_file.rb +8 -3
  85. data/lib/active_support/environment_inquirer.rb +22 -2
  86. data/lib/active_support/error_reporter/test_helper.rb +15 -0
  87. data/lib/active_support/error_reporter.rb +121 -35
  88. data/lib/active_support/execution_wrapper.rb +4 -4
  89. data/lib/active_support/file_update_checker.rb +4 -2
  90. data/lib/active_support/fork_tracker.rb +10 -2
  91. data/lib/active_support/gem_version.rb +4 -4
  92. data/lib/active_support/gzip.rb +2 -0
  93. data/lib/active_support/hash_with_indifferent_access.rb +35 -17
  94. data/lib/active_support/i18n.rb +1 -1
  95. data/lib/active_support/i18n_railtie.rb +20 -13
  96. data/lib/active_support/inflector/inflections.rb +2 -0
  97. data/lib/active_support/inflector/methods.rb +22 -10
  98. data/lib/active_support/inflector/transliterate.rb +3 -1
  99. data/lib/active_support/isolated_execution_state.rb +26 -22
  100. data/lib/active_support/json/decoding.rb +2 -1
  101. data/lib/active_support/json/encoding.rb +25 -43
  102. data/lib/active_support/key_generator.rb +9 -1
  103. data/lib/active_support/lazy_load_hooks.rb +6 -4
  104. data/lib/active_support/locale/en.yml +2 -0
  105. data/lib/active_support/log_subscriber.rb +78 -33
  106. data/lib/active_support/logger.rb +1 -1
  107. data/lib/active_support/logger_thread_safe_level.rb +9 -21
  108. data/lib/active_support/message_encryptor.rb +197 -53
  109. data/lib/active_support/message_encryptors.rb +140 -0
  110. data/lib/active_support/message_pack/cache_serializer.rb +23 -0
  111. data/lib/active_support/message_pack/extensions.rb +292 -0
  112. data/lib/active_support/message_pack/serializer.rb +63 -0
  113. data/lib/active_support/message_pack.rb +50 -0
  114. data/lib/active_support/message_verifier.rb +212 -93
  115. data/lib/active_support/message_verifiers.rb +134 -0
  116. data/lib/active_support/messages/codec.rb +65 -0
  117. data/lib/active_support/messages/metadata.rb +111 -45
  118. data/lib/active_support/messages/rotation_coordinator.rb +93 -0
  119. data/lib/active_support/messages/rotator.rb +34 -32
  120. data/lib/active_support/messages/serializer_with_fallback.rb +158 -0
  121. data/lib/active_support/multibyte/chars.rb +2 -0
  122. data/lib/active_support/multibyte/unicode.rb +9 -37
  123. data/lib/active_support/notifications/fanout.rb +239 -81
  124. data/lib/active_support/notifications/instrumenter.rb +71 -14
  125. data/lib/active_support/notifications.rb +1 -1
  126. data/lib/active_support/number_helper/number_converter.rb +2 -2
  127. data/lib/active_support/number_helper/number_to_human_size_converter.rb +1 -1
  128. data/lib/active_support/number_helper/number_to_phone_converter.rb +1 -0
  129. data/lib/active_support/ordered_hash.rb +3 -3
  130. data/lib/active_support/ordered_options.rb +14 -0
  131. data/lib/active_support/parameter_filter.rb +84 -69
  132. data/lib/active_support/proxy_object.rb +2 -0
  133. data/lib/active_support/railtie.rb +33 -21
  134. data/lib/active_support/reloader.rb +12 -4
  135. data/lib/active_support/rescuable.rb +2 -0
  136. data/lib/active_support/secure_compare_rotator.rb +16 -9
  137. data/lib/active_support/string_inquirer.rb +3 -1
  138. data/lib/active_support/subscriber.rb +9 -27
  139. data/lib/active_support/syntax_error_proxy.rb +49 -0
  140. data/lib/active_support/tagged_logging.rb +60 -24
  141. data/lib/active_support/test_case.rb +153 -6
  142. data/lib/active_support/testing/assertions.rb +25 -9
  143. data/lib/active_support/testing/autorun.rb +0 -2
  144. data/lib/active_support/testing/constant_stubbing.rb +32 -0
  145. data/lib/active_support/testing/deprecation.rb +25 -25
  146. data/lib/active_support/testing/error_reporter_assertions.rb +108 -0
  147. data/lib/active_support/testing/isolation.rb +1 -1
  148. data/lib/active_support/testing/method_call_assertions.rb +21 -8
  149. data/lib/active_support/testing/parallelize_executor.rb +8 -3
  150. data/lib/active_support/testing/stream.rb +1 -1
  151. data/lib/active_support/testing/strict_warnings.rb +38 -0
  152. data/lib/active_support/testing/time_helpers.rb +32 -14
  153. data/lib/active_support/time_with_zone.rb +4 -14
  154. data/lib/active_support/values/time_zone.rb +9 -7
  155. data/lib/active_support/version.rb +1 -1
  156. data/lib/active_support/xml_mini/jdom.rb +3 -10
  157. data/lib/active_support/xml_mini/nokogiri.rb +1 -1
  158. data/lib/active_support/xml_mini/nokogirisax.rb +1 -1
  159. data/lib/active_support/xml_mini/rexml.rb +1 -1
  160. data/lib/active_support/xml_mini.rb +2 -2
  161. data/lib/active_support.rb +13 -3
  162. metadata +106 -21
  163. data/lib/active_support/core_ext/array/deprecated_conversions.rb +0 -25
  164. data/lib/active_support/core_ext/date/deprecated_conversions.rb +0 -40
  165. data/lib/active_support/core_ext/date_time/deprecated_conversions.rb +0 -36
  166. data/lib/active_support/core_ext/numeric/deprecated_conversions.rb +0 -60
  167. data/lib/active_support/core_ext/range/deprecated_conversions.rb +0 -36
  168. data/lib/active_support/core_ext/range/include_time_with_zone.rb +0 -5
  169. data/lib/active_support/core_ext/time/deprecated_conversions.rb +0 -73
  170. data/lib/active_support/core_ext/uri.rb +0 -5
  171. 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