activesupport 7.0.4 → 7.1.5.1

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 (189) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1076 -230
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +6 -6
  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 +30 -5
  8. data/lib/active_support/benchmarkable.rb +1 -0
  9. data/lib/active_support/broadcast_logger.rb +251 -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 +333 -253
  21. data/lib/active_support/callbacks.rb +44 -21
  22. data/lib/active_support/code_generator.rb +15 -10
  23. data/lib/active_support/concern.rb +4 -2
  24. data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +42 -3
  25. data/lib/active_support/concurrency/null_lock.rb +13 -0
  26. data/lib/active_support/configurable.rb +10 -0
  27. data/lib/active_support/core_ext/array/conversions.rb +2 -1
  28. data/lib/active_support/core_ext/array.rb +0 -1
  29. data/lib/active_support/core_ext/class/subclasses.rb +13 -10
  30. data/lib/active_support/core_ext/date/calculations.rb +15 -0
  31. data/lib/active_support/core_ext/date/conversions.rb +2 -1
  32. data/lib/active_support/core_ext/date.rb +0 -1
  33. data/lib/active_support/core_ext/date_and_time/calculations.rb +10 -0
  34. data/lib/active_support/core_ext/date_time/calculations.rb +4 -0
  35. data/lib/active_support/core_ext/date_time/conversions.rb +6 -2
  36. data/lib/active_support/core_ext/date_time.rb +0 -1
  37. data/lib/active_support/core_ext/digest/uuid.rb +1 -10
  38. data/lib/active_support/core_ext/enumerable.rb +8 -75
  39. data/lib/active_support/core_ext/erb/util.rb +196 -0
  40. data/lib/active_support/core_ext/hash/conversions.rb +1 -1
  41. data/lib/active_support/core_ext/hash/deep_merge.rb +22 -14
  42. data/lib/active_support/core_ext/hash/deep_transform_values.rb +3 -3
  43. data/lib/active_support/core_ext/hash/keys.rb +3 -3
  44. data/lib/active_support/core_ext/integer/inflections.rb +12 -12
  45. data/lib/active_support/core_ext/module/attribute_accessors.rb +6 -0
  46. data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +34 -16
  47. data/lib/active_support/core_ext/module/concerning.rb +6 -6
  48. data/lib/active_support/core_ext/module/delegation.rb +81 -37
  49. data/lib/active_support/core_ext/module/deprecation.rb +15 -12
  50. data/lib/active_support/core_ext/module/introspection.rb +0 -1
  51. data/lib/active_support/core_ext/numeric/bytes.rb +9 -0
  52. data/lib/active_support/core_ext/numeric/conversions.rb +2 -0
  53. data/lib/active_support/core_ext/numeric.rb +0 -1
  54. data/lib/active_support/core_ext/object/deep_dup.rb +16 -0
  55. data/lib/active_support/core_ext/object/duplicable.rb +25 -16
  56. data/lib/active_support/core_ext/object/inclusion.rb +13 -5
  57. data/lib/active_support/core_ext/object/instance_variables.rb +22 -12
  58. data/lib/active_support/core_ext/object/json.rb +16 -6
  59. data/lib/active_support/core_ext/object/to_query.rb +0 -2
  60. data/lib/active_support/core_ext/object/with.rb +44 -0
  61. data/lib/active_support/core_ext/object/with_options.rb +9 -9
  62. data/lib/active_support/core_ext/object.rb +1 -0
  63. data/lib/active_support/core_ext/pathname/blank.rb +16 -0
  64. data/lib/active_support/core_ext/pathname/existence.rb +2 -0
  65. data/lib/active_support/core_ext/pathname.rb +1 -0
  66. data/lib/active_support/core_ext/range/conversions.rb +28 -7
  67. data/lib/active_support/core_ext/range/overlap.rb +40 -0
  68. data/lib/active_support/core_ext/range.rb +1 -2
  69. data/lib/active_support/core_ext/securerandom.rb +24 -12
  70. data/lib/active_support/core_ext/string/filters.rb +20 -14
  71. data/lib/active_support/core_ext/string/indent.rb +1 -1
  72. data/lib/active_support/core_ext/string/inflections.rb +16 -9
  73. data/lib/active_support/core_ext/string/output_safety.rb +42 -174
  74. data/lib/active_support/core_ext/thread/backtrace/location.rb +12 -0
  75. data/lib/active_support/core_ext/time/calculations.rb +22 -2
  76. data/lib/active_support/core_ext/time/conversions.rb +2 -2
  77. data/lib/active_support/core_ext/time/zones.rb +7 -8
  78. data/lib/active_support/core_ext/time.rb +0 -1
  79. data/lib/active_support/current_attributes.rb +15 -6
  80. data/lib/active_support/deep_mergeable.rb +53 -0
  81. data/lib/active_support/dependencies/autoload.rb +17 -12
  82. data/lib/active_support/deprecation/behaviors.rb +65 -42
  83. data/lib/active_support/deprecation/constant_accessor.rb +5 -4
  84. data/lib/active_support/deprecation/deprecators.rb +104 -0
  85. data/lib/active_support/deprecation/disallowed.rb +6 -8
  86. data/lib/active_support/deprecation/instance_delegator.rb +31 -4
  87. data/lib/active_support/deprecation/method_wrappers.rb +6 -23
  88. data/lib/active_support/deprecation/proxy_wrappers.rb +37 -22
  89. data/lib/active_support/deprecation/reporting.rb +43 -26
  90. data/lib/active_support/deprecation.rb +32 -5
  91. data/lib/active_support/deprecator.rb +7 -0
  92. data/lib/active_support/descendants_tracker.rb +104 -132
  93. data/lib/active_support/duration/iso8601_serializer.rb +0 -2
  94. data/lib/active_support/duration.rb +2 -1
  95. data/lib/active_support/encrypted_configuration.rb +63 -11
  96. data/lib/active_support/encrypted_file.rb +16 -12
  97. data/lib/active_support/environment_inquirer.rb +22 -2
  98. data/lib/active_support/error_reporter/test_helper.rb +15 -0
  99. data/lib/active_support/error_reporter.rb +121 -35
  100. data/lib/active_support/evented_file_update_checker.rb +17 -2
  101. data/lib/active_support/execution_wrapper.rb +4 -4
  102. data/lib/active_support/file_update_checker.rb +4 -2
  103. data/lib/active_support/fork_tracker.rb +10 -2
  104. data/lib/active_support/gem_version.rb +4 -4
  105. data/lib/active_support/gzip.rb +2 -0
  106. data/lib/active_support/hash_with_indifferent_access.rb +35 -17
  107. data/lib/active_support/html_safe_translation.rb +16 -6
  108. data/lib/active_support/i18n.rb +1 -1
  109. data/lib/active_support/i18n_railtie.rb +20 -13
  110. data/lib/active_support/inflector/inflections.rb +2 -0
  111. data/lib/active_support/inflector/methods.rb +28 -18
  112. data/lib/active_support/inflector/transliterate.rb +3 -1
  113. data/lib/active_support/isolated_execution_state.rb +26 -22
  114. data/lib/active_support/json/decoding.rb +2 -1
  115. data/lib/active_support/json/encoding.rb +25 -43
  116. data/lib/active_support/key_generator.rb +9 -1
  117. data/lib/active_support/lazy_load_hooks.rb +7 -5
  118. data/lib/active_support/locale/en.yml +2 -0
  119. data/lib/active_support/log_subscriber.rb +85 -33
  120. data/lib/active_support/logger.rb +9 -60
  121. data/lib/active_support/logger_thread_safe_level.rb +10 -24
  122. data/lib/active_support/message_encryptor.rb +197 -53
  123. data/lib/active_support/message_encryptors.rb +141 -0
  124. data/lib/active_support/message_pack/cache_serializer.rb +23 -0
  125. data/lib/active_support/message_pack/extensions.rb +292 -0
  126. data/lib/active_support/message_pack/serializer.rb +63 -0
  127. data/lib/active_support/message_pack.rb +50 -0
  128. data/lib/active_support/message_verifier.rb +212 -93
  129. data/lib/active_support/message_verifiers.rb +135 -0
  130. data/lib/active_support/messages/codec.rb +65 -0
  131. data/lib/active_support/messages/metadata.rb +111 -45
  132. data/lib/active_support/messages/rotation_coordinator.rb +93 -0
  133. data/lib/active_support/messages/rotator.rb +34 -32
  134. data/lib/active_support/messages/serializer_with_fallback.rb +158 -0
  135. data/lib/active_support/multibyte/chars.rb +2 -0
  136. data/lib/active_support/multibyte/unicode.rb +9 -37
  137. data/lib/active_support/notifications/fanout.rb +245 -81
  138. data/lib/active_support/notifications/instrumenter.rb +87 -22
  139. data/lib/active_support/notifications.rb +3 -3
  140. data/lib/active_support/number_helper/number_converter.rb +14 -5
  141. data/lib/active_support/number_helper/number_to_currency_converter.rb +6 -6
  142. data/lib/active_support/number_helper/number_to_human_size_converter.rb +3 -3
  143. data/lib/active_support/number_helper/number_to_phone_converter.rb +1 -0
  144. data/lib/active_support/number_helper.rb +379 -317
  145. data/lib/active_support/ordered_hash.rb +3 -3
  146. data/lib/active_support/ordered_options.rb +14 -0
  147. data/lib/active_support/parameter_filter.rb +103 -84
  148. data/lib/active_support/proxy_object.rb +2 -0
  149. data/lib/active_support/railtie.rb +33 -21
  150. data/lib/active_support/reloader.rb +12 -4
  151. data/lib/active_support/rescuable.rb +2 -0
  152. data/lib/active_support/secure_compare_rotator.rb +16 -9
  153. data/lib/active_support/string_inquirer.rb +3 -1
  154. data/lib/active_support/subscriber.rb +9 -27
  155. data/lib/active_support/syntax_error_proxy.rb +60 -0
  156. data/lib/active_support/tagged_logging.rb +64 -24
  157. data/lib/active_support/test_case.rb +153 -6
  158. data/lib/active_support/testing/assertions.rb +26 -10
  159. data/lib/active_support/testing/autorun.rb +0 -2
  160. data/lib/active_support/testing/constant_stubbing.rb +32 -0
  161. data/lib/active_support/testing/deprecation.rb +25 -25
  162. data/lib/active_support/testing/error_reporter_assertions.rb +107 -0
  163. data/lib/active_support/testing/isolation.rb +29 -28
  164. data/lib/active_support/testing/method_call_assertions.rb +21 -8
  165. data/lib/active_support/testing/parallelize_executor.rb +8 -3
  166. data/lib/active_support/testing/setup_and_teardown.rb +2 -0
  167. data/lib/active_support/testing/stream.rb +1 -1
  168. data/lib/active_support/testing/strict_warnings.rb +39 -0
  169. data/lib/active_support/testing/time_helpers.rb +37 -15
  170. data/lib/active_support/time_with_zone.rb +8 -37
  171. data/lib/active_support/values/time_zone.rb +18 -7
  172. data/lib/active_support/version.rb +1 -1
  173. data/lib/active_support/xml_mini/jdom.rb +3 -10
  174. data/lib/active_support/xml_mini/nokogiri.rb +1 -1
  175. data/lib/active_support/xml_mini/nokogirisax.rb +1 -1
  176. data/lib/active_support/xml_mini/rexml.rb +1 -1
  177. data/lib/active_support/xml_mini.rb +2 -2
  178. data/lib/active_support.rb +14 -3
  179. metadata +148 -19
  180. data/lib/active_support/core_ext/array/deprecated_conversions.rb +0 -25
  181. data/lib/active_support/core_ext/date/deprecated_conversions.rb +0 -26
  182. data/lib/active_support/core_ext/date_time/deprecated_conversions.rb +0 -22
  183. data/lib/active_support/core_ext/numeric/deprecated_conversions.rb +0 -60
  184. data/lib/active_support/core_ext/range/deprecated_conversions.rb +0 -26
  185. data/lib/active_support/core_ext/range/include_time_with_zone.rb +0 -7
  186. data/lib/active_support/core_ext/range/overlaps.rb +0 -10
  187. data/lib/active_support/core_ext/time/deprecated_conversions.rb +0 -22
  188. data/lib/active_support/core_ext/uri.rb +0 -5
  189. 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)
@@ -790,7 +1038,8 @@ module ActiveSupport
790
1038
  # When an entry has a positive :race_condition_ttl defined, put the stale entry back into the cache
791
1039
  # for a brief period while the entry is being recalculated.
792
1040
  entry.expires_at = Time.now.to_f + race_ttl
793
- write_entry(key, entry, expires_in: race_ttl * 2)
1041
+ options[:expires_in] = race_ttl * 2
1042
+ write_entry(key, entry, **options)
794
1043
  else
795
1044
  delete_entry(key, **options)
796
1045
  end
@@ -800,13 +1049,15 @@ module ActiveSupport
800
1049
  end
801
1050
 
802
1051
  def get_entry_value(entry, name, options)
803
- instrument(:fetch_hit, name, options) { }
1052
+ instrument(:fetch_hit, name, options)
804
1053
  entry.value
805
1054
  end
806
1055
 
807
1056
  def save_block_result_to_cache(name, options)
1057
+ options = options.dup
1058
+
808
1059
  result = instrument(:generate, name, options) do
809
- yield(name)
1060
+ yield(name, WriteOptions.new(options))
810
1061
  end
811
1062
 
812
1063
  write(name, result, options) unless result.nil? && options[:skip_nil]
@@ -814,217 +1065,46 @@ module ActiveSupport
814
1065
  end
815
1066
  end
816
1067
 
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
1068
+ # Enables the dynamic configuration of Cache entry options while ensuring
1069
+ # that conflicting options are not both set. When a block is given to
1070
+ # ActiveSupport::Cache::Store#fetch, the second argument will be an
1071
+ # instance of +WriteOptions+.
1072
+ class WriteOptions
1073
+ def initialize(options) # :nodoc:
1074
+ @options = options
921
1075
  end
922
1076
 
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
1077
+ def version
1078
+ @options[:version]
933
1079
  end
934
1080
 
935
- def value
936
- compressed? ? uncompress(@value) : @value
1081
+ def version=(version)
1082
+ @options[:version] = version
937
1083
  end
938
1084
 
939
- def mismatched?(version)
940
- @version && version && @version != version
1085
+ def expires_in
1086
+ @options[:expires_in]
941
1087
  end
942
1088
 
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
1089
+ # Sets the Cache entry's +expires_in+ value. If an +expires_at+ option was
1090
+ # previously set, this will unset it since +expires_in+ and +expires_at+
1091
+ # cannot both be set.
1092
+ def expires_in=(expires_in)
1093
+ @options.delete(:expires_at)
1094
+ @options[:expires_in] = expires_in
947
1095
  end
948
1096
 
949
1097
  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
1098
+ @options[:expires_at]
1004
1099
  end
1005
1100
 
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
1101
+ # Sets the Cache entry's +expires_at+ value. If an +expires_in+ option was
1102
+ # previously set, this will unset it since +expires_at+ and +expires_in+
1103
+ # cannot both be set.
1104
+ def expires_at=(expires_at)
1105
+ @options.delete(:expires_in)
1106
+ @options[:expires_at] = expires_at
1016
1107
  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
1108
  end
1029
1109
  end
1030
1110
  end