activesupport 6.1.7.2 → 7.0.5

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activesupport might be problematic. Click here for more details.

Files changed (182) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +261 -511
  3. data/lib/active_support/actionable_error.rb +1 -1
  4. data/lib/active_support/array_inquirer.rb +0 -2
  5. data/lib/active_support/backtrace_cleaner.rb +2 -2
  6. data/lib/active_support/benchmarkable.rb +2 -2
  7. data/lib/active_support/cache/file_store.rb +15 -9
  8. data/lib/active_support/cache/mem_cache_store.rb +148 -37
  9. data/lib/active_support/cache/memory_store.rb +24 -16
  10. data/lib/active_support/cache/null_store.rb +10 -2
  11. data/lib/active_support/cache/redis_cache_store.rb +59 -78
  12. data/lib/active_support/cache/strategy/local_cache.rb +38 -61
  13. data/lib/active_support/cache.rb +299 -147
  14. data/lib/active_support/callbacks.rb +184 -85
  15. data/lib/active_support/code_generator.rb +65 -0
  16. data/lib/active_support/concern.rb +5 -5
  17. data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +2 -4
  18. data/lib/active_support/concurrency/share_lock.rb +2 -2
  19. data/lib/active_support/configurable.rb +8 -5
  20. data/lib/active_support/configuration_file.rb +1 -1
  21. data/lib/active_support/core_ext/array/access.rb +1 -5
  22. data/lib/active_support/core_ext/array/conversions.rb +13 -12
  23. data/lib/active_support/core_ext/array/deprecated_conversions.rb +25 -0
  24. data/lib/active_support/core_ext/array/grouping.rb +6 -6
  25. data/lib/active_support/core_ext/array/inquiry.rb +2 -2
  26. data/lib/active_support/core_ext/array.rb +1 -0
  27. data/lib/active_support/core_ext/big_decimal/conversions.rb +1 -1
  28. data/lib/active_support/core_ext/class/subclasses.rb +25 -17
  29. data/lib/active_support/core_ext/date/blank.rb +1 -1
  30. data/lib/active_support/core_ext/date/calculations.rb +24 -9
  31. data/lib/active_support/core_ext/date/conversions.rb +14 -14
  32. data/lib/active_support/core_ext/date/deprecated_conversions.rb +26 -0
  33. data/lib/active_support/core_ext/date.rb +1 -0
  34. data/lib/active_support/core_ext/date_and_time/calculations.rb +4 -4
  35. data/lib/active_support/core_ext/date_and_time/compatibility.rb +1 -1
  36. data/lib/active_support/core_ext/date_time/blank.rb +1 -1
  37. data/lib/active_support/core_ext/date_time/calculations.rb +4 -0
  38. data/lib/active_support/core_ext/date_time/conversions.rb +13 -13
  39. data/lib/active_support/core_ext/date_time/deprecated_conversions.rb +22 -0
  40. data/lib/active_support/core_ext/date_time.rb +1 -0
  41. data/lib/active_support/core_ext/digest/uuid.rb +39 -14
  42. data/lib/active_support/core_ext/enumerable.rb +106 -37
  43. data/lib/active_support/core_ext/file/atomic.rb +3 -1
  44. data/lib/active_support/core_ext/hash/conversions.rb +0 -1
  45. data/lib/active_support/core_ext/hash/deep_transform_values.rb +3 -3
  46. data/lib/active_support/core_ext/hash/indifferent_access.rb +3 -3
  47. data/lib/active_support/core_ext/hash/keys.rb +4 -4
  48. data/lib/active_support/core_ext/integer/inflections.rb +12 -12
  49. data/lib/active_support/core_ext/kernel/reporting.rb +4 -4
  50. data/lib/active_support/core_ext/kernel/singleton_class.rb +1 -1
  51. data/lib/active_support/core_ext/module/attribute_accessors.rb +2 -0
  52. data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +19 -10
  53. data/lib/active_support/core_ext/module/delegation.rb +2 -8
  54. data/lib/active_support/core_ext/name_error.rb +2 -8
  55. data/lib/active_support/core_ext/numeric/conversions.rb +80 -77
  56. data/lib/active_support/core_ext/numeric/deprecated_conversions.rb +60 -0
  57. data/lib/active_support/core_ext/numeric.rb +1 -0
  58. data/lib/active_support/core_ext/object/acts_like.rb +29 -5
  59. data/lib/active_support/core_ext/object/blank.rb +2 -2
  60. data/lib/active_support/core_ext/object/deep_dup.rb +1 -1
  61. data/lib/active_support/core_ext/object/duplicable.rb +15 -4
  62. data/lib/active_support/core_ext/object/json.rb +30 -25
  63. data/lib/active_support/core_ext/object/to_query.rb +2 -2
  64. data/lib/active_support/core_ext/object/try.rb +20 -20
  65. data/lib/active_support/core_ext/object/with_options.rb +21 -2
  66. data/lib/active_support/core_ext/pathname/existence.rb +21 -0
  67. data/lib/active_support/core_ext/pathname.rb +3 -0
  68. data/lib/active_support/core_ext/range/compare_range.rb +0 -25
  69. data/lib/active_support/core_ext/range/conversions.rb +8 -8
  70. data/lib/active_support/core_ext/range/deprecated_conversions.rb +26 -0
  71. data/lib/active_support/core_ext/range/each.rb +1 -1
  72. data/lib/active_support/core_ext/range/include_time_with_zone.rb +3 -26
  73. data/lib/active_support/core_ext/range/overlaps.rb +1 -1
  74. data/lib/active_support/core_ext/range.rb +1 -1
  75. data/lib/active_support/core_ext/securerandom.rb +1 -1
  76. data/lib/active_support/core_ext/string/conversions.rb +2 -2
  77. data/lib/active_support/core_ext/string/filters.rb +1 -1
  78. data/lib/active_support/core_ext/string/inflections.rb +1 -1
  79. data/lib/active_support/core_ext/string/inquiry.rb +1 -1
  80. data/lib/active_support/core_ext/string/output_safety.rb +66 -38
  81. data/lib/active_support/core_ext/symbol/starts_ends_with.rb +0 -8
  82. data/lib/active_support/core_ext/time/calculations.rb +11 -8
  83. data/lib/active_support/core_ext/time/conversions.rb +13 -12
  84. data/lib/active_support/core_ext/time/deprecated_conversions.rb +22 -0
  85. data/lib/active_support/core_ext/time/zones.rb +10 -26
  86. data/lib/active_support/core_ext/time.rb +1 -0
  87. data/lib/active_support/core_ext/uri.rb +3 -27
  88. data/lib/active_support/core_ext.rb +1 -0
  89. data/lib/active_support/current_attributes.rb +31 -15
  90. data/lib/active_support/dependencies/interlock.rb +10 -18
  91. data/lib/active_support/dependencies/require_dependency.rb +28 -0
  92. data/lib/active_support/dependencies.rb +58 -788
  93. data/lib/active_support/deprecation/behaviors.rb +8 -5
  94. data/lib/active_support/deprecation/disallowed.rb +3 -3
  95. data/lib/active_support/deprecation/method_wrappers.rb +3 -3
  96. data/lib/active_support/deprecation/proxy_wrappers.rb +2 -2
  97. data/lib/active_support/deprecation.rb +2 -2
  98. data/lib/active_support/descendants_tracker.rb +174 -68
  99. data/lib/active_support/digest.rb +4 -4
  100. data/lib/active_support/duration/iso8601_parser.rb +3 -3
  101. data/lib/active_support/duration/iso8601_serializer.rb +9 -1
  102. data/lib/active_support/duration.rb +77 -48
  103. data/lib/active_support/encrypted_configuration.rb +45 -3
  104. data/lib/active_support/encrypted_file.rb +13 -1
  105. data/lib/active_support/environment_inquirer.rb +1 -1
  106. data/lib/active_support/error_reporter.rb +117 -0
  107. data/lib/active_support/evented_file_update_checker.rb +3 -5
  108. data/lib/active_support/execution_context/test_helper.rb +13 -0
  109. data/lib/active_support/execution_context.rb +53 -0
  110. data/lib/active_support/execution_wrapper.rb +30 -11
  111. data/lib/active_support/executor/test_helper.rb +7 -0
  112. data/lib/active_support/fork_tracker.rb +19 -12
  113. data/lib/active_support/gem_version.rb +5 -5
  114. data/lib/active_support/hash_with_indifferent_access.rb +3 -1
  115. data/lib/active_support/html_safe_translation.rb +43 -0
  116. data/lib/active_support/i18n.rb +1 -0
  117. data/lib/active_support/i18n_railtie.rb +1 -1
  118. data/lib/active_support/inflector/inflections.rb +23 -7
  119. data/lib/active_support/inflector/methods.rb +27 -50
  120. data/lib/active_support/inflector/transliterate.rb +1 -1
  121. data/lib/active_support/isolated_execution_state.rb +72 -0
  122. data/lib/active_support/json/encoding.rb +3 -3
  123. data/lib/active_support/key_generator.rb +22 -5
  124. data/lib/active_support/lazy_load_hooks.rb +28 -4
  125. data/lib/active_support/locale/en.yml +1 -1
  126. data/lib/active_support/log_subscriber/test_helper.rb +2 -2
  127. data/lib/active_support/log_subscriber.rb +15 -5
  128. data/lib/active_support/logger_thread_safe_level.rb +4 -13
  129. data/lib/active_support/message_encryptor.rb +12 -6
  130. data/lib/active_support/message_verifier.rb +46 -14
  131. data/lib/active_support/messages/metadata.rb +2 -2
  132. data/lib/active_support/multibyte/chars.rb +10 -11
  133. data/lib/active_support/multibyte/unicode.rb +0 -12
  134. data/lib/active_support/multibyte.rb +1 -1
  135. data/lib/active_support/notifications/fanout.rb +91 -65
  136. data/lib/active_support/notifications/instrumenter.rb +32 -15
  137. data/lib/active_support/notifications.rb +23 -23
  138. data/lib/active_support/number_helper/number_converter.rb +1 -3
  139. data/lib/active_support/number_helper/number_to_currency_converter.rb +11 -6
  140. data/lib/active_support/number_helper/number_to_delimited_converter.rb +1 -1
  141. data/lib/active_support/number_helper/number_to_human_size_converter.rb +1 -1
  142. data/lib/active_support/number_helper/number_to_phone_converter.rb +1 -1
  143. data/lib/active_support/number_helper/rounding_helper.rb +1 -5
  144. data/lib/active_support/number_helper.rb +4 -5
  145. data/lib/active_support/option_merger.rb +10 -18
  146. data/lib/active_support/ordered_hash.rb +1 -1
  147. data/lib/active_support/ordered_options.rb +1 -1
  148. data/lib/active_support/parameter_filter.rb +20 -11
  149. data/lib/active_support/per_thread_registry.rb +5 -1
  150. data/lib/active_support/railtie.rb +69 -19
  151. data/lib/active_support/rescuable.rb +12 -12
  152. data/lib/active_support/ruby_features.rb +7 -0
  153. data/lib/active_support/secure_compare_rotator.rb +2 -2
  154. data/lib/active_support/string_inquirer.rb +0 -2
  155. data/lib/active_support/subscriber.rb +7 -18
  156. data/lib/active_support/tagged_logging.rb +1 -1
  157. data/lib/active_support/test_case.rb +13 -21
  158. data/lib/active_support/testing/assertions.rb +35 -5
  159. data/lib/active_support/testing/deprecation.rb +52 -1
  160. data/lib/active_support/testing/isolation.rb +30 -29
  161. data/lib/active_support/testing/method_call_assertions.rb +5 -5
  162. data/lib/active_support/testing/parallelization/server.rb +4 -0
  163. data/lib/active_support/testing/parallelization/worker.rb +3 -0
  164. data/lib/active_support/testing/parallelization.rb +4 -0
  165. data/lib/active_support/testing/parallelize_executor.rb +76 -0
  166. data/lib/active_support/testing/stream.rb +3 -5
  167. data/lib/active_support/testing/tagged_logging.rb +1 -1
  168. data/lib/active_support/testing/time_helpers.rb +13 -2
  169. data/lib/active_support/time_with_zone.rb +62 -22
  170. data/lib/active_support/values/time_zone.rb +33 -14
  171. data/lib/active_support/version.rb +1 -1
  172. data/lib/active_support/xml_mini/jdom.rb +1 -1
  173. data/lib/active_support/xml_mini/libxml.rb +5 -5
  174. data/lib/active_support/xml_mini/libxmlsax.rb +1 -1
  175. data/lib/active_support/xml_mini/nokogiri.rb +4 -4
  176. data/lib/active_support/xml_mini/nokogirisax.rb +1 -1
  177. data/lib/active_support/xml_mini/rexml.rb +1 -1
  178. data/lib/active_support/xml_mini.rb +5 -4
  179. data/lib/active_support.rb +16 -0
  180. metadata +28 -26
  181. data/lib/active_support/core_ext/marshal.rb +0 -26
  182. data/lib/active_support/dependencies/zeitwerk_integration.rb +0 -120
@@ -22,13 +22,24 @@ module ActiveSupport
22
22
 
23
23
  # These options mean something to all cache implementations. Individual cache
24
24
  # implementations may support additional options.
25
- UNIVERSAL_OPTIONS = [:namespace, :compress, :compress_threshold, :expires_in, :race_condition_ttl, :coder]
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
28
+
29
+ # Mapping of canonical option names to aliases that a store will recognize.
30
+ OPTION_ALIASES = {
31
+ expires_in: [:expire_in, :expired_in]
32
+ }.freeze
26
33
 
27
34
  module Strategy
28
35
  autoload :LocalCache, "active_support/cache/strategy/local_cache"
29
36
  end
30
37
 
38
+ @format_version = 6.1
39
+
31
40
  class << self
41
+ attr_accessor :format_version
42
+
32
43
  # Creates a new Store object according to the given options.
33
44
  #
34
45
  # If no arguments are passed to this method, then a new
@@ -128,9 +139,10 @@ module ActiveSupport
128
139
  # popular cache store for large production websites.
129
140
  #
130
141
  # Some implementations may not support all methods beyond the basic cache
131
- # methods of +fetch+, +write+, +read+, +exist?+, and +delete+.
142
+ # methods of #fetch, #write, #read, #exist?, and #delete.
132
143
  #
133
- # ActiveSupport::Cache::Store can store any serializable Ruby object.
144
+ # ActiveSupport::Cache::Store can store any Ruby object that is supported by
145
+ # its +coder+'s +dump+ and +load+ methods.
134
146
  #
135
147
  # cache = ActiveSupport::Cache::MemoryStore.new
136
148
  #
@@ -138,6 +150,8 @@ module ActiveSupport
138
150
  # cache.write('city', "Duckburgh")
139
151
  # cache.read('city') # => "Duckburgh"
140
152
  #
153
+ # cache.write('not serializable', Proc.new {}) # => TypeError
154
+ #
141
155
  # Keys are always translated into Strings and are case sensitive. When an
142
156
  # object is specified as a key and has a +cache_key+ method defined, this
143
157
  # method will be called to define the key. Otherwise, the +to_param+
@@ -158,14 +172,7 @@ module ActiveSupport
158
172
  # cache.namespace = -> { @last_mod_time } # Set the namespace to a variable
159
173
  # @last_mod_time = Time.now # Invalidate the entire cache by changing namespace
160
174
  #
161
- # Cached data larger than 1kB are compressed by default. To turn off
162
- # compression, pass <tt>compress: false</tt> to the initializer or to
163
- # individual +fetch+ or +write+ method calls. The 1kB compression
164
- # threshold is configurable with the <tt>:compress_threshold</tt> option,
165
- # specified in bytes.
166
175
  class Store
167
- DEFAULT_CODER = Marshal
168
-
169
176
  cattr_accessor :logger, instance_writer: true
170
177
 
171
178
  attr_reader :silence, :options
@@ -188,12 +195,26 @@ module ActiveSupport
188
195
  end
189
196
  end
190
197
 
191
- # Creates a new cache. The options will be passed to any write method calls
192
- # except for <tt>:namespace</tt> which can be used to set the global
193
- # namespace for the cache.
198
+ # Creates a new cache.
199
+ #
200
+ # ==== Options
201
+ #
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.
208
+ #
209
+ # Any other specified options are treated as default options for the
210
+ # relevant cache operations, such as #read, #write, and #fetch.
194
211
  def initialize(options = nil)
195
- @options = options ? options.dup : {}
196
- @coder = @options.delete(:coder) { self.class::DEFAULT_CODER } || NullCoder
212
+ @options = options ? normalize_options(options) : {}
213
+ @options[:compress] = true unless @options.key?(:compress)
214
+ @options[:compress_threshold] = DEFAULT_COMPRESS_LIMIT unless @options.key?(:compress_threshold)
215
+
216
+ @coder = @options.delete(:coder) { default_coder } || NullCoder
217
+ @coder_supports_compression = @coder.respond_to?(:dump_compressed)
197
218
  end
198
219
 
199
220
  # Silences the logger.
@@ -228,101 +249,75 @@ module ActiveSupport
228
249
  # end
229
250
  # cache.fetch('city') # => "Duckburgh"
230
251
  #
231
- # You may also specify additional options via the +options+ argument.
232
- # Setting <tt>force: true</tt> forces a cache "miss," meaning we treat
233
- # the cache value as missing even if it's present. Passing a block is
234
- # required when +force+ is true so this always results in a cache write.
252
+ # ==== Options
235
253
  #
236
- # cache.write('today', 'Monday')
237
- # cache.fetch('today', force: true) { 'Tuesday' } # => 'Tuesday'
238
- # cache.fetch('today', force: true) # => ArgumentError
239
- #
240
- # The +:force+ option is useful when you're calling some other method to
241
- # ask whether you should force a cache write. Otherwise, it's clearer to
242
- # just call <tt>Cache#write</tt>.
243
- #
244
- # Setting <tt>skip_nil: true</tt> will not cache nil result:
245
- #
246
- # cache.fetch('foo') { nil }
247
- # cache.fetch('bar', skip_nil: true) { nil }
248
- # cache.exist?('foo') # => true
249
- # cache.exist?('bar') # => false
250
- #
251
- #
252
- # Setting <tt>compress: false</tt> disables compression of the cache entry.
253
- #
254
- # Setting <tt>:expires_in</tt> will set an expiration time on the cache.
255
- # All caches support auto-expiring content after a specified number of
256
- # seconds. This value can be specified as an option to the constructor
257
- # (in which case all entries will be affected), or it can be supplied to
258
- # the +fetch+ or +write+ method to effect just one entry.
259
- #
260
- # cache = ActiveSupport::Cache::MemoryStore.new(expires_in: 5.minutes)
261
- # cache.write(key, value, expires_in: 1.minute) # Set a lower value for one entry
262
- #
263
- # Setting <tt>:version</tt> verifies the cache stored under <tt>name</tt>
264
- # is of the same version. nil is returned on mismatches despite contents.
265
- # This feature is used to support recyclable cache keys.
266
- #
267
- # Setting <tt>:race_condition_ttl</tt> is very useful in situations where
268
- # a cache entry is used very frequently and is under heavy load. If a
269
- # cache expires and due to heavy load several different processes will try
270
- # to read data natively and then they all will try to write to cache. To
271
- # avoid that case the first process to find an expired cache entry will
272
- # bump the cache expiration time by the value set in <tt>:race_condition_ttl</tt>.
273
- # Yes, this process is extending the time for a stale value by another few
274
- # seconds. Because of extended life of the previous cache, other processes
275
- # will continue to use slightly stale data for a just a bit longer. In the
276
- # meantime that first process will go ahead and will write into cache the
277
- # new value. After that all the processes will start getting the new value.
278
- # The key is to keep <tt>:race_condition_ttl</tt> small.
279
- #
280
- # If the process regenerating the entry errors out, the entry will be
281
- # regenerated after the specified number of seconds. Also note that the
282
- # life of stale cache is extended only if it expired recently. Otherwise
283
- # a new value is generated and <tt>:race_condition_ttl</tt> does not play
284
- # any role.
285
- #
286
- # # Set all values to expire after one minute.
287
- # cache = ActiveSupport::Cache::MemoryStore.new(expires_in: 1.minute)
288
- #
289
- # cache.write('foo', 'original value')
290
- # val_1 = nil
291
- # val_2 = nil
292
- # sleep 60
293
- #
294
- # Thread.new do
295
- # val_1 = cache.fetch('foo', race_condition_ttl: 10.seconds) do
296
- # sleep 1
297
- # 'new value 1'
298
- # end
299
- # end
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.
256
+ # Additionally, +fetch+ supports the following options:
300
257
  #
301
- # Thread.new do
302
- # val_2 = cache.fetch('foo', race_condition_ttl: 10.seconds) do
303
- # 'new value 2'
304
- # end
305
- # end
258
+ # * <tt>force: true</tt> - Forces a cache "miss," meaning we treat the
259
+ # cache value as missing even if it's present. Passing a block is
260
+ # required when +force+ is true so this always results in a cache write.
306
261
  #
307
- # cache.fetch('foo') # => "original value"
308
- # sleep 10 # First thread extended the life of cache by another 10 seconds
309
- # cache.fetch('foo') # => "new value 1"
310
- # val_1 # => "new value 1"
311
- # val_2 # => "original value"
262
+ # cache.write('today', 'Monday')
263
+ # cache.fetch('today', force: true) { 'Tuesday' } # => 'Tuesday'
264
+ # cache.fetch('today', force: true) # => ArgumentError
312
265
  #
313
- # Other options will be handled by the specific cache store implementation.
314
- # Internally, #fetch calls #read_entry, and calls #write_entry on a cache
315
- # miss. +options+ will be passed to the #read and #write calls.
266
+ # The +:force+ option is useful when you're calling some other method to
267
+ # ask whether you should force a cache write. Otherwise, it's clearer to
268
+ # just call +write+.
316
269
  #
317
- # For example, MemCacheStore's #write method supports the +:raw+
318
- # option, which tells the memcached server to store all values as strings.
319
- # We can use this option with #fetch too:
270
+ # * <tt>skip_nil: true</tt> - Prevents caching a nil result:
271
+ #
272
+ # cache.fetch('foo') { nil }
273
+ # cache.fetch('bar', skip_nil: true) { nil }
274
+ # cache.exist?('foo') # => true
275
+ # cache.exist?('bar') # => false
276
+ #
277
+ # * +:race_condition_ttl+ - Specifies the number of seconds during which
278
+ # an expired value can be reused while a new value is being generated.
279
+ # This can be used to prevent race conditions when cache entries expire,
280
+ # by preventing multiple processes from simultaneously regenerating the
281
+ # same entry (also known as the dog pile effect).
282
+ #
283
+ # When a process encounters a cache entry that has expired less than
284
+ # +:race_condition_ttl+ seconds ago, it will bump the expiration time by
285
+ # +:race_condition_ttl+ seconds before generating a new value. During
286
+ # this extended time window, while the process generates a new value,
287
+ # other processes will continue to use the old value. After the first
288
+ # process writes the new value, other processes will then use it.
289
+ #
290
+ # If the first process errors out while generating a new value, another
291
+ # process can try to generate a new value after the extended time window
292
+ # has elapsed.
293
+ #
294
+ # # Set all values to expire after one minute.
295
+ # cache = ActiveSupport::Cache::MemoryStore.new(expires_in: 1.minute)
296
+ #
297
+ # cache.write('foo', 'original value')
298
+ # val_1 = nil
299
+ # val_2 = nil
300
+ # sleep 60
301
+ #
302
+ # Thread.new do
303
+ # val_1 = cache.fetch('foo', race_condition_ttl: 10.seconds) do
304
+ # sleep 1
305
+ # 'new value 1'
306
+ # end
307
+ # end
308
+ #
309
+ # Thread.new do
310
+ # val_2 = cache.fetch('foo', race_condition_ttl: 10.seconds) do
311
+ # 'new value 2'
312
+ # end
313
+ # end
314
+ #
315
+ # cache.fetch('foo') # => "original value"
316
+ # sleep 10 # First thread extended the life of cache by another 10 seconds
317
+ # cache.fetch('foo') # => "new value 1"
318
+ # val_1 # => "new value 1"
319
+ # val_2 # => "original value"
320
320
  #
321
- # cache = ActiveSupport::Cache::MemCacheStore.new
322
- # cache.fetch("foo", force: true, raw: true) do
323
- # :bar
324
- # end
325
- # cache.fetch('foo') # => "bar"
326
321
  def fetch(name, options = nil, &block)
327
322
  if block_given?
328
323
  options = merged_options(options)
@@ -357,7 +352,13 @@ module ActiveSupport
357
352
  # <tt>:version</tt> options, both of these conditions are applied before
358
353
  # the data is returned.
359
354
  #
360
- # Options are passed to the underlying cache implementation.
355
+ # ==== Options
356
+ #
357
+ # * +:version+ - Specifies a version for the cache entry. If the cached
358
+ # version does not match the requested version, the read will be treated
359
+ # as a cache miss. This feature is used to support recyclable cache keys.
360
+ #
361
+ # Other options will be handled by the specific cache store implementation.
361
362
  def read(name, options = nil)
362
363
  options = merged_options(options)
363
364
  key = normalize_key(name, options)
@@ -465,9 +466,39 @@ module ActiveSupport
465
466
  end
466
467
  end
467
468
 
468
- # Writes the value to the cache, with the key.
469
+ # Writes the value to the cache with the key. The value must be supported
470
+ # by the +coder+'s +dump+ and +load+ methods.
469
471
  #
470
- # Options are passed to the underlying cache implementation.
472
+ # By default, cache entries larger than 1kB are compressed. Compression
473
+ # allows more data to be stored in the same memory footprint, leading to
474
+ # fewer cache evictions and higher hit rates.
475
+ #
476
+ # ==== Options
477
+ #
478
+ # * <tt>compress: false</tt> - Disables compression of the cache entry.
479
+ #
480
+ # * +:compress_threshold+ - The compression threshold, specified in bytes.
481
+ # \Cache entries larger than this threshold will be compressed. Defaults
482
+ # to +1.kilobyte+.
483
+ #
484
+ # * +:expires_in+ - Sets a relative expiration time for the cache entry,
485
+ # specified in seconds. +:expire_in+ and +:expired_in+ are aliases for
486
+ # +:expires_in+.
487
+ #
488
+ # cache = ActiveSupport::Cache::MemoryStore.new(expires_in: 5.minutes)
489
+ # cache.write(key, value, expires_in: 1.minute) # Set a lower value for one entry
490
+ #
491
+ # * +:expires_at+ - Sets an absolute expiration time for the cache entry.
492
+ #
493
+ # cache = ActiveSupport::Cache::MemoryStore.new
494
+ # cache.write(key, value, expires_at: Time.now.at_end_of_hour)
495
+ #
496
+ # * +:version+ - Specifies a version for the cache entry. When reading
497
+ # from the cache, if the cached version does not match the requested
498
+ # version, the read will be treated as a cache miss. This feature is
499
+ # used to support recyclable cache keys.
500
+ #
501
+ # Other options will be handled by the specific cache store implementation.
471
502
  def write(name, value, options = nil)
472
503
  options = merged_options(options)
473
504
 
@@ -512,6 +543,10 @@ module ActiveSupport
512
543
  end
513
544
  end
514
545
 
546
+ def new_entry(value, options = nil) # :nodoc:
547
+ Entry.new(value, **merged_options(options))
548
+ end
549
+
515
550
  # Deletes all entries with keys matching the pattern.
516
551
  #
517
552
  # Options are passed to the underlying cache implementation.
@@ -539,7 +574,7 @@ module ActiveSupport
539
574
  raise NotImplementedError.new("#{self.class.name} does not support decrement")
540
575
  end
541
576
 
542
- # Cleanups the cache by removing expired entries.
577
+ # Cleans up the cache by removing expired entries.
543
578
  #
544
579
  # Options are passed to the underlying cache implementation.
545
580
  #
@@ -559,6 +594,10 @@ module ActiveSupport
559
594
  end
560
595
 
561
596
  private
597
+ def default_coder
598
+ Coders[Cache.format_version]
599
+ end
600
+
562
601
  # Adds the namespace defined in the options to a pattern designed to
563
602
  # match keys. Implementations that support delete_matched should call
564
603
  # this method to translate a pattern that matches names into one that
@@ -590,8 +629,13 @@ module ActiveSupport
590
629
  raise NotImplementedError.new
591
630
  end
592
631
 
593
- def serialize_entry(entry)
594
- @coder.dump(entry)
632
+ def serialize_entry(entry, **options)
633
+ options = merged_options(options)
634
+ if @coder_supports_compression && options[:compress]
635
+ @coder.dump_compressed(entry, options[:compress_threshold] || DEFAULT_COMPRESS_LIMIT)
636
+ else
637
+ @coder.dump(entry)
638
+ end
595
639
  end
596
640
 
597
641
  def deserialize_entry(payload)
@@ -640,6 +684,7 @@ module ActiveSupport
640
684
  # Merges the default options with ones specific to a method call.
641
685
  def merged_options(call_options)
642
686
  if call_options
687
+ call_options = normalize_options(call_options)
643
688
  if options.empty?
644
689
  call_options
645
690
  else
@@ -650,6 +695,18 @@ module ActiveSupport
650
695
  end
651
696
  end
652
697
 
698
+ # Normalize aliased options to their canonical form
699
+ def normalize_options(options)
700
+ options = options.dup
701
+ OPTION_ALIASES.each do |canonical_name, aliases|
702
+ alias_key = aliases.detect { |key| options.key?(key) }
703
+ options[canonical_name] ||= options[alias_key] if alias_key
704
+ options.except!(*aliases)
705
+ end
706
+
707
+ options
708
+ end
709
+
653
710
  # Expands and namespaces the cache key. May be overridden by
654
711
  # cache stores to do additional normalization.
655
712
  def normalize_key(key, options = nil)
@@ -732,7 +789,7 @@ module ActiveSupport
732
789
  if (race_ttl > 0) && (Time.now.to_f - entry.expires_at <= race_ttl)
733
790
  # When an entry has a positive :race_condition_ttl defined, put the stale entry back into the cache
734
791
  # for a brief period while the entry is being recalculated.
735
- entry.expires_at = Time.now + race_ttl
792
+ entry.expires_at = Time.now.to_f + race_ttl
736
793
  write_entry(key, entry, expires_in: race_ttl * 2)
737
794
  else
738
795
  delete_entry(key, **options)
@@ -758,13 +815,93 @@ module ActiveSupport
758
815
  end
759
816
 
760
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
+
761
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
+
762
854
  def load(payload)
763
- 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)
764
871
  end
872
+ end
873
+
874
+ module Rails61Coder
875
+ include Loader
876
+ extend self
765
877
 
766
878
  def dump(entry)
767
- 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
768
905
  end
769
906
  end
770
907
  end
@@ -777,19 +914,22 @@ module ActiveSupport
777
914
  # Since cache entries in most instances will be serialized, the internals of this class are highly optimized
778
915
  # using short instance variable names that are lazily defined.
779
916
  class Entry # :nodoc:
780
- attr_reader :version
917
+ class << self
918
+ def unpack(members)
919
+ new(members[0], expires_at: members[1], version: members[2])
920
+ end
921
+ end
781
922
 
782
- DEFAULT_COMPRESS_LIMIT = 1.kilobyte
923
+ attr_reader :version
783
924
 
784
925
  # Creates a new cache entry for the specified value. Options supported are
785
- # +:compress+, +:compress_threshold+, +:version+ and +:expires_in+.
786
- def initialize(value, compress: true, compress_threshold: DEFAULT_COMPRESS_LIMIT, version: nil, expires_in: nil, **)
926
+ # +:compressed+, +:version+, +:expires_at+ and +:expires_in+.
927
+ def initialize(value, compressed: false, version: nil, expires_in: nil, expires_at: nil, **)
787
928
  @value = value
788
929
  @version = version
789
- @created_at = Time.now.to_f
790
- @expires_in = expires_in && expires_in.to_f
791
-
792
- compress!(compress_threshold) if compress
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
793
933
  end
794
934
 
795
935
  def value
@@ -831,6 +971,38 @@ module ActiveSupport
831
971
  end
832
972
  end
833
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
1004
+ end
1005
+
834
1006
  # Duplicates the value in a class. This is used by cache implementations that don't natively
835
1007
  # serialize entries to protect against accidental cache modifications.
836
1008
  def dup_value!
@@ -843,33 +1015,13 @@ module ActiveSupport
843
1015
  end
844
1016
  end
845
1017
 
846
- private
847
- def compress!(compress_threshold)
848
- case @value
849
- when nil, true, false, Numeric
850
- uncompressed_size = 0
851
- when String
852
- uncompressed_size = @value.bytesize
853
- else
854
- serialized = Marshal.dump(@value)
855
- uncompressed_size = serialized.bytesize
856
- end
857
-
858
- if uncompressed_size >= compress_threshold
859
- serialized ||= Marshal.dump(@value)
860
- compressed = Zlib::Deflate.deflate(serialized)
861
-
862
- if compressed.bytesize < uncompressed_size
863
- @value = compressed
864
- @compressed = true
865
- end
866
- end
867
- end
868
-
869
- def compressed?
870
- defined?(@compressed)
871
- end
1018
+ def pack
1019
+ members = [value, expires_at, version]
1020
+ members.pop while !members.empty? && members.last.nil?
1021
+ members
1022
+ end
872
1023
 
1024
+ private
873
1025
  def uncompress(value)
874
1026
  Marshal.load(Zlib::Inflate.inflate(value))
875
1027
  end