activesupport 7.0.6 → 7.1.3.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (180) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +967 -263
  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 +250 -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 +134 -0
  13. data/lib/active_support/cache/file_store.rb +37 -10
  14. data/lib/active_support/cache/mem_cache_store.rb +100 -76
  15. data/lib/active_support/cache/memory_store.rb +78 -24
  16. data/lib/active_support/cache/null_store.rb +6 -0
  17. data/lib/active_support/cache/redis_cache_store.rb +153 -141
  18. data/lib/active_support/cache/serializer_with_fallback.rb +175 -0
  19. data/lib/active_support/cache/strategy/local_cache.rb +29 -14
  20. data/lib/active_support/cache.rb +331 -252
  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 +2 -1
  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 -70
  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/concerning.rb +6 -6
  42. data/lib/active_support/core_ext/module/delegation.rb +40 -11
  43. data/lib/active_support/core_ext/module/deprecation.rb +15 -12
  44. data/lib/active_support/core_ext/module/introspection.rb +0 -1
  45. data/lib/active_support/core_ext/numeric/bytes.rb +9 -0
  46. data/lib/active_support/core_ext/numeric/conversions.rb +2 -0
  47. data/lib/active_support/core_ext/numeric.rb +0 -1
  48. data/lib/active_support/core_ext/object/deep_dup.rb +16 -0
  49. data/lib/active_support/core_ext/object/inclusion.rb +13 -5
  50. data/lib/active_support/core_ext/object/instance_variables.rb +22 -12
  51. data/lib/active_support/core_ext/object/json.rb +11 -3
  52. data/lib/active_support/core_ext/object/with.rb +44 -0
  53. data/lib/active_support/core_ext/object/with_options.rb +4 -4
  54. data/lib/active_support/core_ext/object.rb +1 -0
  55. data/lib/active_support/core_ext/pathname/blank.rb +16 -0
  56. data/lib/active_support/core_ext/pathname/existence.rb +2 -0
  57. data/lib/active_support/core_ext/pathname.rb +1 -0
  58. data/lib/active_support/core_ext/range/conversions.rb +28 -7
  59. data/lib/active_support/core_ext/range/overlap.rb +40 -0
  60. data/lib/active_support/core_ext/range.rb +1 -2
  61. data/lib/active_support/core_ext/securerandom.rb +24 -12
  62. data/lib/active_support/core_ext/string/filters.rb +20 -14
  63. data/lib/active_support/core_ext/string/indent.rb +1 -1
  64. data/lib/active_support/core_ext/string/inflections.rb +16 -5
  65. data/lib/active_support/core_ext/string/output_safety.rb +38 -174
  66. data/lib/active_support/core_ext/thread/backtrace/location.rb +12 -0
  67. data/lib/active_support/core_ext/time/calculations.rb +18 -2
  68. data/lib/active_support/core_ext/time/conversions.rb +2 -2
  69. data/lib/active_support/core_ext/time/zones.rb +4 -4
  70. data/lib/active_support/core_ext/time.rb +0 -1
  71. data/lib/active_support/current_attributes.rb +15 -6
  72. data/lib/active_support/deep_mergeable.rb +53 -0
  73. data/lib/active_support/dependencies/autoload.rb +17 -12
  74. data/lib/active_support/deprecation/behaviors.rb +65 -42
  75. data/lib/active_support/deprecation/constant_accessor.rb +5 -4
  76. data/lib/active_support/deprecation/deprecators.rb +104 -0
  77. data/lib/active_support/deprecation/disallowed.rb +3 -5
  78. data/lib/active_support/deprecation/instance_delegator.rb +31 -4
  79. data/lib/active_support/deprecation/method_wrappers.rb +6 -23
  80. data/lib/active_support/deprecation/proxy_wrappers.rb +37 -22
  81. data/lib/active_support/deprecation/reporting.rb +42 -25
  82. data/lib/active_support/deprecation.rb +32 -5
  83. data/lib/active_support/deprecator.rb +7 -0
  84. data/lib/active_support/descendants_tracker.rb +104 -132
  85. data/lib/active_support/duration/iso8601_serializer.rb +0 -2
  86. data/lib/active_support/duration.rb +2 -1
  87. data/lib/active_support/encrypted_configuration.rb +30 -9
  88. data/lib/active_support/encrypted_file.rb +16 -12
  89. data/lib/active_support/environment_inquirer.rb +22 -2
  90. data/lib/active_support/error_reporter/test_helper.rb +15 -0
  91. data/lib/active_support/error_reporter.rb +121 -35
  92. data/lib/active_support/evented_file_update_checker.rb +3 -1
  93. data/lib/active_support/execution_wrapper.rb +4 -4
  94. data/lib/active_support/file_update_checker.rb +4 -2
  95. data/lib/active_support/fork_tracker.rb +10 -2
  96. data/lib/active_support/gem_version.rb +4 -4
  97. data/lib/active_support/gzip.rb +2 -0
  98. data/lib/active_support/hash_with_indifferent_access.rb +35 -17
  99. data/lib/active_support/html_safe_translation.rb +12 -2
  100. data/lib/active_support/i18n.rb +1 -1
  101. data/lib/active_support/i18n_railtie.rb +20 -13
  102. data/lib/active_support/inflector/inflections.rb +2 -0
  103. data/lib/active_support/inflector/methods.rb +24 -12
  104. data/lib/active_support/inflector/transliterate.rb +3 -1
  105. data/lib/active_support/isolated_execution_state.rb +26 -22
  106. data/lib/active_support/json/decoding.rb +2 -1
  107. data/lib/active_support/json/encoding.rb +25 -43
  108. data/lib/active_support/key_generator.rb +9 -1
  109. data/lib/active_support/lazy_load_hooks.rb +6 -4
  110. data/lib/active_support/locale/en.yml +2 -0
  111. data/lib/active_support/log_subscriber.rb +84 -33
  112. data/lib/active_support/logger.rb +9 -60
  113. data/lib/active_support/logger_thread_safe_level.rb +10 -24
  114. data/lib/active_support/message_encryptor.rb +197 -53
  115. data/lib/active_support/message_encryptors.rb +141 -0
  116. data/lib/active_support/message_pack/cache_serializer.rb +23 -0
  117. data/lib/active_support/message_pack/extensions.rb +292 -0
  118. data/lib/active_support/message_pack/serializer.rb +63 -0
  119. data/lib/active_support/message_pack.rb +50 -0
  120. data/lib/active_support/message_verifier.rb +212 -93
  121. data/lib/active_support/message_verifiers.rb +135 -0
  122. data/lib/active_support/messages/codec.rb +65 -0
  123. data/lib/active_support/messages/metadata.rb +111 -45
  124. data/lib/active_support/messages/rotation_coordinator.rb +93 -0
  125. data/lib/active_support/messages/rotator.rb +34 -32
  126. data/lib/active_support/messages/serializer_with_fallback.rb +158 -0
  127. data/lib/active_support/multibyte/chars.rb +2 -0
  128. data/lib/active_support/multibyte/unicode.rb +9 -37
  129. data/lib/active_support/notifications/fanout.rb +245 -81
  130. data/lib/active_support/notifications/instrumenter.rb +77 -20
  131. data/lib/active_support/notifications.rb +1 -1
  132. data/lib/active_support/number_helper/number_converter.rb +14 -5
  133. data/lib/active_support/number_helper/number_to_currency_converter.rb +6 -6
  134. data/lib/active_support/number_helper/number_to_human_size_converter.rb +3 -3
  135. data/lib/active_support/number_helper/number_to_phone_converter.rb +1 -0
  136. data/lib/active_support/number_helper.rb +379 -318
  137. data/lib/active_support/ordered_hash.rb +3 -3
  138. data/lib/active_support/ordered_options.rb +14 -0
  139. data/lib/active_support/parameter_filter.rb +84 -69
  140. data/lib/active_support/proxy_object.rb +2 -0
  141. data/lib/active_support/railtie.rb +33 -21
  142. data/lib/active_support/reloader.rb +12 -4
  143. data/lib/active_support/rescuable.rb +2 -0
  144. data/lib/active_support/secure_compare_rotator.rb +16 -9
  145. data/lib/active_support/string_inquirer.rb +3 -1
  146. data/lib/active_support/subscriber.rb +9 -27
  147. data/lib/active_support/syntax_error_proxy.rb +70 -0
  148. data/lib/active_support/tagged_logging.rb +60 -24
  149. data/lib/active_support/test_case.rb +153 -6
  150. data/lib/active_support/testing/assertions.rb +26 -10
  151. data/lib/active_support/testing/autorun.rb +0 -2
  152. data/lib/active_support/testing/constant_stubbing.rb +32 -0
  153. data/lib/active_support/testing/deprecation.rb +25 -25
  154. data/lib/active_support/testing/error_reporter_assertions.rb +107 -0
  155. data/lib/active_support/testing/isolation.rb +1 -1
  156. data/lib/active_support/testing/method_call_assertions.rb +21 -8
  157. data/lib/active_support/testing/parallelize_executor.rb +8 -3
  158. data/lib/active_support/testing/stream.rb +1 -1
  159. data/lib/active_support/testing/strict_warnings.rb +39 -0
  160. data/lib/active_support/testing/time_helpers.rb +37 -15
  161. data/lib/active_support/time_with_zone.rb +6 -35
  162. data/lib/active_support/values/time_zone.rb +9 -7
  163. data/lib/active_support/version.rb +1 -1
  164. data/lib/active_support/xml_mini/jdom.rb +3 -10
  165. data/lib/active_support/xml_mini/nokogiri.rb +1 -1
  166. data/lib/active_support/xml_mini/nokogirisax.rb +1 -1
  167. data/lib/active_support/xml_mini/rexml.rb +1 -1
  168. data/lib/active_support/xml_mini.rb +2 -2
  169. data/lib/active_support.rb +14 -3
  170. metadata +106 -19
  171. data/lib/active_support/core_ext/array/deprecated_conversions.rb +0 -25
  172. data/lib/active_support/core_ext/date/deprecated_conversions.rb +0 -26
  173. data/lib/active_support/core_ext/date_time/deprecated_conversions.rb +0 -22
  174. data/lib/active_support/core_ext/numeric/deprecated_conversions.rb +0 -60
  175. data/lib/active_support/core_ext/range/deprecated_conversions.rb +0 -26
  176. data/lib/active_support/core_ext/range/include_time_with_zone.rb +0 -5
  177. data/lib/active_support/core_ext/range/overlaps.rb +0 -10
  178. data/lib/active_support/core_ext/time/deprecated_conversions.rb +0 -22
  179. data/lib/active_support/core_ext/uri.rb +0 -5
  180. 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.
@@ -141,8 +160,8 @@ module ActiveSupport
141
160
  # Some implementations may not support all methods beyond the basic cache
142
161
  # methods of #fetch, #write, #read, #exist?, and #delete.
143
162
  #
144
- # ActiveSupport::Cache::Store can store any Ruby object that is supported by
145
- # its +coder+'s +dump+ and +load+ methods.
163
+ # +ActiveSupport::Cache::Store+ can store any Ruby object that is supported
164
+ # by its +coder+'s +dump+ and +load+ methods.
146
165
  #
147
166
  # cache = ActiveSupport::Cache::MemoryStore.new
148
167
  #
@@ -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
 
@@ -251,8 +370,8 @@ module ActiveSupport
251
370
  #
252
371
  # ==== Options
253
372
  #
254
- # Internally, +fetch+ calls #read_entry, and calls #write_entry on a cache
255
- # miss. Thus, +fetch+ supports the same options as #read and #write.
373
+ # Internally, +fetch+ calls +read_entry+, and calls +write_entry+ on a
374
+ # cache miss. Thus, +fetch+ supports the same options as #read and #write.
256
375
  # Additionally, +fetch+ supports the following options:
257
376
  #
258
377
  # * <tt>force: true</tt> - Forces a cache "miss," meaning we treat the
@@ -318,18 +437,42 @@ 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
+ if entry
463
+ if entry.mismatched?(normalize_version(name, options))
464
+ entry = nil
465
+ else
466
+ begin
467
+ entry.value
468
+ rescue DeserializationError
469
+ entry = nil
470
+ end
471
+ end
472
+ end
473
+ payload[:super_operation] = :fetch if payload
474
+ payload[:hit] = !!entry if payload
475
+ end
333
476
  end
334
477
 
335
478
  if entry
@@ -354,6 +497,7 @@ module ActiveSupport
354
497
  #
355
498
  # ==== Options
356
499
  #
500
+ # * +:namespace+ - Replace the store namespace for this call.
357
501
  # * +:version+ - Specifies a version for the cache entry. If the cached
358
502
  # version does not match the requested version, the read will be treated
359
503
  # as a cache miss. This feature is used to support recyclable cache keys.
@@ -377,7 +521,12 @@ module ActiveSupport
377
521
  nil
378
522
  else
379
523
  payload[:hit] = true if payload
380
- entry.value
524
+ begin
525
+ entry.value
526
+ rescue DeserializationError
527
+ payload[:hit] = false
528
+ nil
529
+ end
381
530
  end
382
531
  else
383
532
  payload[:hit] = false if payload
@@ -393,10 +542,12 @@ module ActiveSupport
393
542
  #
394
543
  # Returns a hash mapping the names provided to the values found.
395
544
  def read_multi(*names)
545
+ return {} if names.empty?
546
+
396
547
  options = names.extract_options!
397
548
  options = merged_options(options)
398
549
 
399
- instrument :read_multi, names, options do |payload|
550
+ instrument_multi :read_multi, names, options do |payload|
400
551
  read_multi_entries(names, **options, event: payload).tap do |results|
401
552
  payload[:hits] = results.keys
402
553
  end
@@ -405,9 +556,11 @@ module ActiveSupport
405
556
 
406
557
  # Cache Storage API to write multiple values at once.
407
558
  def write_multi(hash, options = nil)
559
+ return hash if hash.empty?
560
+
408
561
  options = merged_options(options)
409
562
 
410
- instrument :write_multi, hash, options do |payload|
563
+ instrument_multi :write_multi, hash, options do |payload|
411
564
  entries = hash.each_with_object({}) do |(name, value), memo|
412
565
  memo[normalize_key(name, options)] = Entry.new(value, **options.merge(version: normalize_version(name, options)))
413
566
  end
@@ -433,7 +586,8 @@ module ActiveSupport
433
586
  # # => { "bim" => "bam",
434
587
  # # "unknown_key" => "Fallback value for key: unknown_key" }
435
588
  #
436
- # Options are passed to the underlying cache implementation. For example:
589
+ # You may also specify additional options via the +options+ argument. See #fetch for details.
590
+ # Other options are passed to the underlying cache implementation. For example:
437
591
  #
438
592
  # cache.fetch_multi("fizz", expires_in: 5.seconds) do |key|
439
593
  # "buzz"
@@ -446,16 +600,23 @@ module ActiveSupport
446
600
  # # => nil
447
601
  def fetch_multi(*names)
448
602
  raise ArgumentError, "Missing block: `Cache#fetch_multi` requires a block." unless block_given?
603
+ return {} if names.empty?
449
604
 
450
605
  options = names.extract_options!
451
606
  options = merged_options(options)
452
607
 
453
- instrument :read_multi, names, options do |payload|
454
- reads = read_multi_entries(names, **options)
608
+ instrument_multi :read_multi, names, options do |payload|
609
+ if options[:force]
610
+ reads = {}
611
+ else
612
+ reads = read_multi_entries(names, **options)
613
+ end
614
+
455
615
  writes = {}
456
616
  ordered = names.index_with do |name|
457
617
  reads.fetch(name) { writes[name] = yield(name) }
458
618
  end
619
+ writes.compact! if options[:skip_nil]
459
620
 
460
621
  payload[:hits] = reads.keys
461
622
  payload[:super_operation] = :fetch_multi
@@ -508,7 +669,8 @@ module ActiveSupport
508
669
  end
509
670
  end
510
671
 
511
- # Deletes an entry in the cache. Returns +true+ if an entry is deleted.
672
+ # Deletes an entry in the cache. Returns +true+ if an entry is deleted
673
+ # and +false+ otherwise.
512
674
  #
513
675
  # Options are passed to the underlying cache implementation.
514
676
  def delete(name, options = nil)
@@ -519,14 +681,17 @@ module ActiveSupport
519
681
  end
520
682
  end
521
683
 
522
- # Deletes multiple entries in the cache.
684
+ # Deletes multiple entries in the cache. Returns the number of deleted
685
+ # entries.
523
686
  #
524
687
  # Options are passed to the underlying cache implementation.
525
688
  def delete_multi(names, options = nil)
689
+ return 0 if names.empty?
690
+
526
691
  options = merged_options(options)
527
692
  names.map! { |key| normalize_key(key, options) }
528
693
 
529
- instrument :delete_multi, names do
694
+ instrument_multi :delete_multi, names do
530
695
  delete_multi_entries(names, **options)
531
696
  end
532
697
  end
@@ -594,8 +759,23 @@ module ActiveSupport
594
759
  end
595
760
 
596
761
  private
597
- def default_coder
598
- Coders[Cache.format_version]
762
+ def default_serializer
763
+ case Cache.format_version
764
+ when 6.1
765
+ ActiveSupport.deprecator.warn <<~EOM
766
+ Support for `config.active_support.cache_format_version = 6.1` has been deprecated and will be removed in Rails 7.2.
767
+
768
+ Check the Rails upgrade guide at https://guides.rubyonrails.org/upgrading_ruby_on_rails.html#new-activesupport-cache-serialization-format
769
+ for more information on how to upgrade.
770
+ EOM
771
+ Cache::SerializerWithFallback[:marshal_6_1]
772
+ when 7.0
773
+ Cache::SerializerWithFallback[:marshal_7_0]
774
+ when 7.1
775
+ Cache::SerializerWithFallback[:marshal_7_1]
776
+ else
777
+ raise ArgumentError, "Unrecognized ActiveSupport::Cache.format_version: #{Cache.format_version.inspect}"
778
+ end
599
779
  end
600
780
 
601
781
  # Adds the namespace defined in the options to a pattern designed to
@@ -632,14 +812,16 @@ module ActiveSupport
632
812
  def serialize_entry(entry, **options)
633
813
  options = merged_options(options)
634
814
  if @coder_supports_compression && options[:compress]
635
- @coder.dump_compressed(entry, options[:compress_threshold] || DEFAULT_COMPRESS_LIMIT)
815
+ @coder.dump_compressed(entry, options[:compress_threshold])
636
816
  else
637
817
  @coder.dump(entry)
638
818
  end
639
819
  end
640
820
 
641
- def deserialize_entry(payload)
821
+ def deserialize_entry(payload, **)
642
822
  payload.nil? ? nil : @coder.load(payload)
823
+ rescue DeserializationError
824
+ nil
643
825
  end
644
826
 
645
827
  # Reads multiple entries from the cache implementation. Subclasses MAY
@@ -685,6 +867,22 @@ module ActiveSupport
685
867
  def merged_options(call_options)
686
868
  if call_options
687
869
  call_options = normalize_options(call_options)
870
+ if call_options.key?(:expires_in) && call_options.key?(:expires_at)
871
+ raise ArgumentError, "Either :expires_in or :expires_at can be supplied, but not both"
872
+ end
873
+
874
+ expires_at = call_options.delete(:expires_at)
875
+ call_options[:expires_in] = (expires_at - Time.now) if expires_at
876
+
877
+ if call_options[:expires_in].is_a?(Time)
878
+ expires_in = call_options[:expires_in]
879
+ raise ArgumentError.new("expires_in parameter should not be a Time. Did you mean to use expires_at? Got: #{expires_in}")
880
+ end
881
+ if call_options[:expires_in]&.negative?
882
+ expires_in = call_options.delete(:expires_in)
883
+ handle_invalid_expires_in("Cache expiration time is invalid, cannot be negative: #{expires_in}")
884
+ end
885
+
688
886
  if options.empty?
689
887
  call_options
690
888
  else
@@ -695,6 +893,16 @@ module ActiveSupport
695
893
  end
696
894
  end
697
895
 
896
+ def handle_invalid_expires_in(message)
897
+ error = ArgumentError.new(message)
898
+ if ActiveSupport::Cache::Store.raise_on_invalid_cache_expiration_time
899
+ raise error
900
+ else
901
+ ActiveSupport.error_reporter&.report(error, handled: true, severity: :warning)
902
+ logger.error("#{error.class}: #{error.message}") if logger
903
+ end
904
+ end
905
+
698
906
  # Normalize aliased options to their canonical form
699
907
  def normalize_options(options)
700
908
  options = options.dup
@@ -707,10 +915,31 @@ module ActiveSupport
707
915
  options
708
916
  end
709
917
 
710
- # Expands and namespaces the cache key. May be overridden by
711
- # cache stores to do additional normalization.
918
+ def validate_options(options)
919
+ if options.key?(:coder) && options[:serializer]
920
+ raise ArgumentError, "Cannot specify :serializer and :coder options together"
921
+ end
922
+
923
+ if options.key?(:coder) && options[:compressor]
924
+ raise ArgumentError, "Cannot specify :compressor and :coder options together"
925
+ end
926
+
927
+ if Cache.format_version < 7.1 && !options[:serializer] && options[:compressor]
928
+ raise ArgumentError, "Cannot specify :compressor option when using" \
929
+ " default serializer and cache format version is < 7.1"
930
+ end
931
+
932
+ options
933
+ end
934
+
935
+ # Expands and namespaces the cache key.
936
+ # Raises an exception when the key is +nil+ or an empty string.
937
+ # May be overridden by cache stores to do additional normalization.
712
938
  def normalize_key(key, options = nil)
713
- namespace_key expanded_key(key), options
939
+ str_key = expanded_key(key)
940
+ raise(ArgumentError, "key cannot be blank") if !str_key || str_key.empty?
941
+
942
+ namespace_key str_key, options
714
943
  end
715
944
 
716
945
  # Prefix the key with a namespace string:
@@ -773,14 +1002,33 @@ module ActiveSupport
773
1002
  end
774
1003
  end
775
1004
 
776
- def instrument(operation, key, options = nil)
1005
+ def instrument(operation, key, options = nil, &block)
1006
+ _instrument(operation, key: key, options: options, &block)
1007
+ end
1008
+
1009
+ def instrument_multi(operation, keys, options = nil, &block)
1010
+ _instrument(operation, multi: true, key: keys, options: options, &block)
1011
+ end
1012
+
1013
+ def _instrument(operation, multi: false, options: nil, **payload, &block)
777
1014
  if logger && logger.debug? && !silence?
778
- logger.debug "Cache #{operation}: #{normalize_key(key, options)}#{options.blank? ? "" : " (#{options.inspect})"}"
1015
+ debug_key =
1016
+ if multi
1017
+ ": #{payload[:key].size} key(s) specified"
1018
+ elsif payload[:key]
1019
+ ": #{normalize_key(payload[:key], options)}"
1020
+ end
1021
+
1022
+ debug_options = " (#{options.inspect})" unless options.blank?
1023
+
1024
+ logger.debug "Cache #{operation}#{debug_key}#{debug_options}"
779
1025
  end
780
1026
 
781
- payload = { key: key, store: self.class.name }
1027
+ payload[:store] = self.class.name
782
1028
  payload.merge!(options) if options.is_a?(Hash)
783
- ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload) { yield(payload) }
1029
+ ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload) do
1030
+ block&.call(payload)
1031
+ end
784
1032
  end
785
1033
 
786
1034
  def handle_expired_entry(entry, key, options)
@@ -800,13 +1048,15 @@ module ActiveSupport
800
1048
  end
801
1049
 
802
1050
  def get_entry_value(entry, name, options)
803
- instrument(:fetch_hit, name, options) { }
1051
+ instrument(:fetch_hit, name, options)
804
1052
  entry.value
805
1053
  end
806
1054
 
807
1055
  def save_block_result_to_cache(name, options)
1056
+ options = options.dup
1057
+
808
1058
  result = instrument(:generate, name, options) do
809
- yield(name)
1059
+ yield(name, WriteOptions.new(options))
810
1060
  end
811
1061
 
812
1062
  write(name, result, options) unless result.nil? && options[:skip_nil]
@@ -814,217 +1064,46 @@ module ActiveSupport
814
1064
  end
815
1065
  end
816
1066
 
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
1067
+ # Enables the dynamic configuration of Cache entry options while ensuring
1068
+ # that conflicting options are not both set. When a block is given to
1069
+ # ActiveSupport::Cache::Store#fetch, the second argument will be an
1070
+ # instance of +WriteOptions+.
1071
+ class WriteOptions
1072
+ def initialize(options) # :nodoc:
1073
+ @options = options
921
1074
  end
922
1075
 
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
1076
+ def version
1077
+ @options[:version]
933
1078
  end
934
1079
 
935
- def value
936
- compressed? ? uncompress(@value) : @value
1080
+ def version=(version)
1081
+ @options[:version] = version
937
1082
  end
938
1083
 
939
- def mismatched?(version)
940
- @version && version && @version != version
1084
+ def expires_in
1085
+ @options[:expires_in]
941
1086
  end
942
1087
 
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
1088
+ # Sets the Cache entry's +expires_in+ value. If an +expires_at+ option was
1089
+ # previously set, this will unset it since +expires_in+ and +expires_at+
1090
+ # cannot both be set.
1091
+ def expires_in=(expires_in)
1092
+ @options.delete(:expires_at)
1093
+ @options[:expires_in] = expires_in
947
1094
  end
948
1095
 
949
1096
  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
1097
+ @options[:expires_at]
1004
1098
  end
1005
1099
 
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
1100
+ # Sets the Cache entry's +expires_at+ value. If an +expires_in+ option was
1101
+ # previously set, this will unset it since +expires_at+ and +expires_in+
1102
+ # cannot both be set.
1103
+ def expires_at=(expires_at)
1104
+ @options.delete(:expires_in)
1105
+ @options[:expires_at] = expires_at
1016
1106
  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
1107
  end
1029
1108
  end
1030
1109
  end