activesupport 6.1.0 → 7.0.4.1

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 (183) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +263 -352
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +1 -1
  5. data/lib/active_support/actionable_error.rb +1 -1
  6. data/lib/active_support/array_inquirer.rb +0 -2
  7. data/lib/active_support/backtrace_cleaner.rb +2 -2
  8. data/lib/active_support/benchmarkable.rb +2 -2
  9. data/lib/active_support/cache/file_store.rb +16 -10
  10. data/lib/active_support/cache/mem_cache_store.rb +154 -39
  11. data/lib/active_support/cache/memory_store.rb +24 -16
  12. data/lib/active_support/cache/null_store.rb +10 -2
  13. data/lib/active_support/cache/redis_cache_store.rb +59 -78
  14. data/lib/active_support/cache/strategy/local_cache.rb +38 -61
  15. data/lib/active_support/cache.rb +306 -148
  16. data/lib/active_support/callbacks.rb +184 -85
  17. data/lib/active_support/code_generator.rb +65 -0
  18. data/lib/active_support/concern.rb +5 -5
  19. data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +2 -4
  20. data/lib/active_support/concurrency/share_lock.rb +2 -2
  21. data/lib/active_support/configurable.rb +8 -5
  22. data/lib/active_support/configuration_file.rb +7 -2
  23. data/lib/active_support/core_ext/array/access.rb +1 -5
  24. data/lib/active_support/core_ext/array/conversions.rb +13 -12
  25. data/lib/active_support/core_ext/array/deprecated_conversions.rb +25 -0
  26. data/lib/active_support/core_ext/array/grouping.rb +6 -6
  27. data/lib/active_support/core_ext/array/inquiry.rb +2 -2
  28. data/lib/active_support/core_ext/array.rb +1 -0
  29. data/lib/active_support/core_ext/big_decimal/conversions.rb +1 -1
  30. data/lib/active_support/core_ext/class/subclasses.rb +25 -17
  31. data/lib/active_support/core_ext/date/blank.rb +1 -1
  32. data/lib/active_support/core_ext/date/calculations.rb +9 -9
  33. data/lib/active_support/core_ext/date/conversions.rb +14 -14
  34. data/lib/active_support/core_ext/date/deprecated_conversions.rb +26 -0
  35. data/lib/active_support/core_ext/date.rb +1 -0
  36. data/lib/active_support/core_ext/date_and_time/calculations.rb +4 -4
  37. data/lib/active_support/core_ext/date_and_time/compatibility.rb +1 -1
  38. data/lib/active_support/core_ext/date_time/blank.rb +1 -1
  39. data/lib/active_support/core_ext/date_time/conversions.rb +13 -13
  40. data/lib/active_support/core_ext/date_time/deprecated_conversions.rb +22 -0
  41. data/lib/active_support/core_ext/date_time.rb +1 -0
  42. data/lib/active_support/core_ext/digest/uuid.rb +39 -13
  43. data/lib/active_support/core_ext/enumerable.rb +101 -32
  44. data/lib/active_support/core_ext/file/atomic.rb +3 -1
  45. data/lib/active_support/core_ext/hash/conversions.rb +0 -1
  46. data/lib/active_support/core_ext/hash/indifferent_access.rb +3 -3
  47. data/lib/active_support/core_ext/hash/keys.rb +1 -1
  48. data/lib/active_support/core_ext/kernel/reporting.rb +4 -4
  49. data/lib/active_support/core_ext/kernel/singleton_class.rb +1 -1
  50. data/lib/active_support/core_ext/module/attribute_accessors.rb +2 -0
  51. data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +19 -10
  52. data/lib/active_support/core_ext/module/delegation.rb +2 -8
  53. data/lib/active_support/core_ext/name_error.rb +2 -8
  54. data/lib/active_support/core_ext/numeric/conversions.rb +80 -77
  55. data/lib/active_support/core_ext/numeric/deprecated_conversions.rb +60 -0
  56. data/lib/active_support/core_ext/numeric.rb +1 -0
  57. data/lib/active_support/core_ext/object/acts_like.rb +29 -5
  58. data/lib/active_support/core_ext/object/blank.rb +2 -2
  59. data/lib/active_support/core_ext/object/deep_dup.rb +1 -1
  60. data/lib/active_support/core_ext/object/duplicable.rb +11 -0
  61. data/lib/active_support/core_ext/object/json.rb +37 -25
  62. data/lib/active_support/core_ext/object/to_query.rb +2 -2
  63. data/lib/active_support/core_ext/object/try.rb +20 -20
  64. data/lib/active_support/core_ext/object/with_options.rb +20 -1
  65. data/lib/active_support/core_ext/pathname/existence.rb +21 -0
  66. data/lib/active_support/core_ext/pathname.rb +3 -0
  67. data/lib/active_support/core_ext/range/compare_range.rb +0 -25
  68. data/lib/active_support/core_ext/range/conversions.rb +8 -8
  69. data/lib/active_support/core_ext/range/deprecated_conversions.rb +26 -0
  70. data/lib/active_support/core_ext/range/each.rb +1 -1
  71. data/lib/active_support/core_ext/range/include_time_with_zone.rb +4 -25
  72. data/lib/active_support/core_ext/range/overlaps.rb +1 -1
  73. data/lib/active_support/core_ext/range.rb +1 -1
  74. data/lib/active_support/core_ext/securerandom.rb +1 -1
  75. data/lib/active_support/core_ext/string/conversions.rb +2 -2
  76. data/lib/active_support/core_ext/string/filters.rb +1 -1
  77. data/lib/active_support/core_ext/string/inflections.rb +1 -1
  78. data/lib/active_support/core_ext/string/inquiry.rb +1 -1
  79. data/lib/active_support/core_ext/string/output_safety.rb +91 -39
  80. data/lib/active_support/core_ext/symbol/starts_ends_with.rb +0 -8
  81. data/lib/active_support/core_ext/time/calculations.rb +9 -7
  82. data/lib/active_support/core_ext/time/conversions.rb +14 -12
  83. data/lib/active_support/core_ext/time/deprecated_conversions.rb +22 -0
  84. data/lib/active_support/core_ext/time/zones.rb +7 -22
  85. data/lib/active_support/core_ext/time.rb +1 -0
  86. data/lib/active_support/core_ext/uri.rb +3 -27
  87. data/lib/active_support/core_ext.rb +2 -1
  88. data/lib/active_support/current_attributes.rb +32 -14
  89. data/lib/active_support/dependencies/interlock.rb +10 -18
  90. data/lib/active_support/dependencies/require_dependency.rb +28 -0
  91. data/lib/active_support/dependencies.rb +58 -788
  92. data/lib/active_support/deprecation/behaviors.rb +8 -5
  93. data/lib/active_support/deprecation/method_wrappers.rb +3 -3
  94. data/lib/active_support/deprecation/proxy_wrappers.rb +2 -2
  95. data/lib/active_support/deprecation.rb +2 -2
  96. data/lib/active_support/descendants_tracker.rb +174 -68
  97. data/lib/active_support/digest.rb +5 -3
  98. data/lib/active_support/duration/iso8601_parser.rb +3 -3
  99. data/lib/active_support/duration/iso8601_serializer.rb +9 -1
  100. data/lib/active_support/duration.rb +81 -51
  101. data/lib/active_support/encrypted_configuration.rb +13 -2
  102. data/lib/active_support/encrypted_file.rb +13 -1
  103. data/lib/active_support/environment_inquirer.rb +1 -1
  104. data/lib/active_support/error_reporter.rb +117 -0
  105. data/lib/active_support/evented_file_update_checker.rb +3 -5
  106. data/lib/active_support/execution_context/test_helper.rb +13 -0
  107. data/lib/active_support/execution_context.rb +53 -0
  108. data/lib/active_support/execution_wrapper.rb +43 -21
  109. data/lib/active_support/executor/test_helper.rb +7 -0
  110. data/lib/active_support/fork_tracker.rb +19 -10
  111. data/lib/active_support/gem_version.rb +5 -5
  112. data/lib/active_support/hash_with_indifferent_access.rb +9 -2
  113. data/lib/active_support/html_safe_translation.rb +43 -0
  114. data/lib/active_support/i18n.rb +1 -0
  115. data/lib/active_support/i18n_railtie.rb +1 -1
  116. data/lib/active_support/inflector/inflections.rb +23 -7
  117. data/lib/active_support/inflector/methods.rb +24 -48
  118. data/lib/active_support/inflector/transliterate.rb +1 -1
  119. data/lib/active_support/isolated_execution_state.rb +72 -0
  120. data/lib/active_support/json/encoding.rb +3 -3
  121. data/lib/active_support/key_generator.rb +22 -5
  122. data/lib/active_support/lazy_load_hooks.rb +28 -4
  123. data/lib/active_support/locale/en.yml +2 -2
  124. data/lib/active_support/log_subscriber/test_helper.rb +2 -2
  125. data/lib/active_support/log_subscriber.rb +15 -5
  126. data/lib/active_support/logger_thread_safe_level.rb +4 -13
  127. data/lib/active_support/message_encryptor.rb +12 -6
  128. data/lib/active_support/message_verifier.rb +46 -14
  129. data/lib/active_support/messages/metadata.rb +2 -2
  130. data/lib/active_support/multibyte/chars.rb +10 -11
  131. data/lib/active_support/multibyte/unicode.rb +0 -12
  132. data/lib/active_support/multibyte.rb +1 -1
  133. data/lib/active_support/notifications/fanout.rb +91 -65
  134. data/lib/active_support/notifications/instrumenter.rb +32 -15
  135. data/lib/active_support/notifications.rb +24 -24
  136. data/lib/active_support/number_helper/number_converter.rb +1 -3
  137. data/lib/active_support/number_helper/number_to_currency_converter.rb +11 -6
  138. data/lib/active_support/number_helper/number_to_delimited_converter.rb +1 -1
  139. data/lib/active_support/number_helper/number_to_human_size_converter.rb +1 -1
  140. data/lib/active_support/number_helper/number_to_phone_converter.rb +1 -1
  141. data/lib/active_support/number_helper/number_to_rounded_converter.rb +10 -6
  142. data/lib/active_support/number_helper/rounding_helper.rb +2 -6
  143. data/lib/active_support/number_helper.rb +0 -2
  144. data/lib/active_support/option_merger.rb +10 -18
  145. data/lib/active_support/ordered_hash.rb +1 -1
  146. data/lib/active_support/ordered_options.rb +1 -1
  147. data/lib/active_support/parameter_filter.rb +6 -1
  148. data/lib/active_support/per_thread_registry.rb +5 -0
  149. data/lib/active_support/railtie.rb +69 -19
  150. data/lib/active_support/reloader.rb +1 -1
  151. data/lib/active_support/rescuable.rb +16 -16
  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/security_utils.rb +1 -1
  155. data/lib/active_support/string_inquirer.rb +0 -2
  156. data/lib/active_support/subscriber.rb +7 -18
  157. data/lib/active_support/tagged_logging.rb +2 -2
  158. data/lib/active_support/test_case.rb +13 -21
  159. data/lib/active_support/testing/assertions.rb +36 -6
  160. data/lib/active_support/testing/deprecation.rb +52 -1
  161. data/lib/active_support/testing/isolation.rb +2 -2
  162. data/lib/active_support/testing/method_call_assertions.rb +5 -5
  163. data/lib/active_support/testing/parallelization/server.rb +4 -0
  164. data/lib/active_support/testing/parallelization/worker.rb +3 -0
  165. data/lib/active_support/testing/parallelization.rb +4 -0
  166. data/lib/active_support/testing/parallelize_executor.rb +76 -0
  167. data/lib/active_support/testing/stream.rb +3 -5
  168. data/lib/active_support/testing/tagged_logging.rb +1 -1
  169. data/lib/active_support/testing/time_helpers.rb +13 -2
  170. data/lib/active_support/time_with_zone.rb +60 -20
  171. data/lib/active_support/values/time_zone.rb +36 -15
  172. data/lib/active_support/version.rb +1 -1
  173. data/lib/active_support/xml_mini/jdom.rb +1 -1
  174. data/lib/active_support/xml_mini/libxml.rb +5 -5
  175. data/lib/active_support/xml_mini/libxmlsax.rb +1 -1
  176. data/lib/active_support/xml_mini/nokogiri.rb +4 -4
  177. data/lib/active_support/xml_mini/nokogirisax.rb +1 -1
  178. data/lib/active_support/xml_mini/rexml.rb +1 -1
  179. data/lib/active_support/xml_mini.rb +5 -4
  180. data/lib/active_support.rb +17 -1
  181. metadata +29 -26
  182. data/lib/active_support/core_ext/marshal.rb +0 -26
  183. data/lib/active_support/dependencies/zeitwerk_integration.rb +0 -117
@@ -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
@@ -58,7 +69,13 @@ module ActiveSupport
58
69
  case store
59
70
  when Symbol
60
71
  options = parameters.extract_options!
61
- retrieve_store_class(store).new(*parameters, **options)
72
+ # clean this up once Ruby 2.7 support is dropped
73
+ # see https://github.com/rails/rails/pull/41522#discussion_r581186602
74
+ if options.empty?
75
+ retrieve_store_class(store).new(*parameters)
76
+ else
77
+ retrieve_store_class(store).new(*parameters, **options)
78
+ end
62
79
  when Array
63
80
  lookup_store(*store)
64
81
  when nil
@@ -122,9 +139,10 @@ module ActiveSupport
122
139
  # popular cache store for large production websites.
123
140
  #
124
141
  # Some implementations may not support all methods beyond the basic cache
125
- # methods of +fetch+, +write+, +read+, +exist?+, and +delete+.
142
+ # methods of #fetch, #write, #read, #exist?, and #delete.
126
143
  #
127
- # 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.
128
146
  #
129
147
  # cache = ActiveSupport::Cache::MemoryStore.new
130
148
  #
@@ -132,6 +150,8 @@ module ActiveSupport
132
150
  # cache.write('city', "Duckburgh")
133
151
  # cache.read('city') # => "Duckburgh"
134
152
  #
153
+ # cache.write('not serializable', Proc.new {}) # => TypeError
154
+ #
135
155
  # Keys are always translated into Strings and are case sensitive. When an
136
156
  # object is specified as a key and has a +cache_key+ method defined, this
137
157
  # method will be called to define the key. Otherwise, the +to_param+
@@ -152,14 +172,7 @@ module ActiveSupport
152
172
  # cache.namespace = -> { @last_mod_time } # Set the namespace to a variable
153
173
  # @last_mod_time = Time.now # Invalidate the entire cache by changing namespace
154
174
  #
155
- # Cached data larger than 1kB are compressed by default. To turn off
156
- # compression, pass <tt>compress: false</tt> to the initializer or to
157
- # individual +fetch+ or +write+ method calls. The 1kB compression
158
- # threshold is configurable with the <tt>:compress_threshold</tt> option,
159
- # specified in bytes.
160
175
  class Store
161
- DEFAULT_CODER = Marshal
162
-
163
176
  cattr_accessor :logger, instance_writer: true
164
177
 
165
178
  attr_reader :silence, :options
@@ -182,12 +195,26 @@ module ActiveSupport
182
195
  end
183
196
  end
184
197
 
185
- # Creates a new cache. The options will be passed to any write method calls
186
- # except for <tt>:namespace</tt> which can be used to set the global
187
- # 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.
188
211
  def initialize(options = nil)
189
- @options = options ? options.dup : {}
190
- @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)
191
218
  end
192
219
 
193
220
  # Silences the logger.
@@ -222,101 +249,75 @@ module ActiveSupport
222
249
  # end
223
250
  # cache.fetch('city') # => "Duckburgh"
224
251
  #
225
- # You may also specify additional options via the +options+ argument.
226
- # Setting <tt>force: true</tt> forces a cache "miss," meaning we treat
227
- # the cache value as missing even if it's present. Passing a block is
228
- # required when +force+ is true so this always results in a cache write.
252
+ # ==== Options
229
253
  #
230
- # cache.write('today', 'Monday')
231
- # cache.fetch('today', force: true) { 'Tuesday' } # => 'Tuesday'
232
- # cache.fetch('today', force: true) # => ArgumentError
233
- #
234
- # The +:force+ option is useful when you're calling some other method to
235
- # ask whether you should force a cache write. Otherwise, it's clearer to
236
- # just call <tt>Cache#write</tt>.
237
- #
238
- # Setting <tt>skip_nil: true</tt> will not cache nil result:
239
- #
240
- # cache.fetch('foo') { nil }
241
- # cache.fetch('bar', skip_nil: true) { nil }
242
- # cache.exist?('foo') # => true
243
- # cache.exist?('bar') # => false
244
- #
245
- #
246
- # Setting <tt>compress: false</tt> disables compression of the cache entry.
247
- #
248
- # Setting <tt>:expires_in</tt> will set an expiration time on the cache.
249
- # All caches support auto-expiring content after a specified number of
250
- # seconds. This value can be specified as an option to the constructor
251
- # (in which case all entries will be affected), or it can be supplied to
252
- # the +fetch+ or +write+ method to effect just one entry.
253
- #
254
- # cache = ActiveSupport::Cache::MemoryStore.new(expires_in: 5.minutes)
255
- # cache.write(key, value, expires_in: 1.minute) # Set a lower value for one entry
256
- #
257
- # Setting <tt>:version</tt> verifies the cache stored under <tt>name</tt>
258
- # is of the same version. nil is returned on mismatches despite contents.
259
- # This feature is used to support recyclable cache keys.
260
- #
261
- # Setting <tt>:race_condition_ttl</tt> is very useful in situations where
262
- # a cache entry is used very frequently and is under heavy load. If a
263
- # cache expires and due to heavy load several different processes will try
264
- # to read data natively and then they all will try to write to cache. To
265
- # avoid that case the first process to find an expired cache entry will
266
- # bump the cache expiration time by the value set in <tt>:race_condition_ttl</tt>.
267
- # Yes, this process is extending the time for a stale value by another few
268
- # seconds. Because of extended life of the previous cache, other processes
269
- # will continue to use slightly stale data for a just a bit longer. In the
270
- # meantime that first process will go ahead and will write into cache the
271
- # new value. After that all the processes will start getting the new value.
272
- # The key is to keep <tt>:race_condition_ttl</tt> small.
273
- #
274
- # If the process regenerating the entry errors out, the entry will be
275
- # regenerated after the specified number of seconds. Also note that the
276
- # life of stale cache is extended only if it expired recently. Otherwise
277
- # a new value is generated and <tt>:race_condition_ttl</tt> does not play
278
- # any role.
279
- #
280
- # # Set all values to expire after one minute.
281
- # cache = ActiveSupport::Cache::MemoryStore.new(expires_in: 1.minute)
282
- #
283
- # cache.write('foo', 'original value')
284
- # val_1 = nil
285
- # val_2 = nil
286
- # sleep 60
287
- #
288
- # Thread.new do
289
- # val_1 = cache.fetch('foo', race_condition_ttl: 10.seconds) do
290
- # sleep 1
291
- # 'new value 1'
292
- # end
293
- # 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:
294
257
  #
295
- # Thread.new do
296
- # val_2 = cache.fetch('foo', race_condition_ttl: 10.seconds) do
297
- # 'new value 2'
298
- # end
299
- # 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.
300
261
  #
301
- # cache.fetch('foo') # => "original value"
302
- # sleep 10 # First thread extended the life of cache by another 10 seconds
303
- # cache.fetch('foo') # => "new value 1"
304
- # val_1 # => "new value 1"
305
- # val_2 # => "original value"
262
+ # cache.write('today', 'Monday')
263
+ # cache.fetch('today', force: true) { 'Tuesday' } # => 'Tuesday'
264
+ # cache.fetch('today', force: true) # => ArgumentError
306
265
  #
307
- # Other options will be handled by the specific cache store implementation.
308
- # Internally, #fetch calls #read_entry, and calls #write_entry on a cache
309
- # 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+.
310
269
  #
311
- # For example, MemCacheStore's #write method supports the +:raw+
312
- # option, which tells the memcached server to store all values as strings.
313
- # 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"
314
320
  #
315
- # cache = ActiveSupport::Cache::MemCacheStore.new
316
- # cache.fetch("foo", force: true, raw: true) do
317
- # :bar
318
- # end
319
- # cache.fetch('foo') # => "bar"
320
321
  def fetch(name, options = nil, &block)
321
322
  if block_given?
322
323
  options = merged_options(options)
@@ -351,7 +352,13 @@ module ActiveSupport
351
352
  # <tt>:version</tt> options, both of these conditions are applied before
352
353
  # the data is returned.
353
354
  #
354
- # 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.
355
362
  def read(name, options = nil)
356
363
  options = merged_options(options)
357
364
  key = normalize_key(name, options)
@@ -459,9 +466,39 @@ module ActiveSupport
459
466
  end
460
467
  end
461
468
 
462
- # 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.
463
471
  #
464
- # 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.
465
502
  def write(name, value, options = nil)
466
503
  options = merged_options(options)
467
504
 
@@ -506,6 +543,10 @@ module ActiveSupport
506
543
  end
507
544
  end
508
545
 
546
+ def new_entry(value, options = nil) # :nodoc:
547
+ Entry.new(value, **merged_options(options))
548
+ end
549
+
509
550
  # Deletes all entries with keys matching the pattern.
510
551
  #
511
552
  # Options are passed to the underlying cache implementation.
@@ -533,7 +574,7 @@ module ActiveSupport
533
574
  raise NotImplementedError.new("#{self.class.name} does not support decrement")
534
575
  end
535
576
 
536
- # Cleanups the cache by removing expired entries.
577
+ # Cleans up the cache by removing expired entries.
537
578
  #
538
579
  # Options are passed to the underlying cache implementation.
539
580
  #
@@ -553,6 +594,10 @@ module ActiveSupport
553
594
  end
554
595
 
555
596
  private
597
+ def default_coder
598
+ Coders[Cache.format_version]
599
+ end
600
+
556
601
  # Adds the namespace defined in the options to a pattern designed to
557
602
  # match keys. Implementations that support delete_matched should call
558
603
  # this method to translate a pattern that matches names into one that
@@ -584,8 +629,13 @@ module ActiveSupport
584
629
  raise NotImplementedError.new
585
630
  end
586
631
 
587
- def serialize_entry(entry)
588
- @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
589
639
  end
590
640
 
591
641
  def deserialize_entry(payload)
@@ -634,6 +684,7 @@ module ActiveSupport
634
684
  # Merges the default options with ones specific to a method call.
635
685
  def merged_options(call_options)
636
686
  if call_options
687
+ call_options = normalize_options(call_options)
637
688
  if options.empty?
638
689
  call_options
639
690
  else
@@ -644,6 +695,18 @@ module ActiveSupport
644
695
  end
645
696
  end
646
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
+
647
710
  # Expands and namespaces the cache key. May be overridden by
648
711
  # cache stores to do additional normalization.
649
712
  def normalize_key(key, options = nil)
@@ -726,7 +789,7 @@ module ActiveSupport
726
789
  if (race_ttl > 0) && (Time.now.to_f - entry.expires_at <= race_ttl)
727
790
  # When an entry has a positive :race_condition_ttl defined, put the stale entry back into the cache
728
791
  # for a brief period while the entry is being recalculated.
729
- entry.expires_at = Time.now + race_ttl
792
+ entry.expires_at = Time.now.to_f + race_ttl
730
793
  write_entry(key, entry, expires_in: race_ttl * 2)
731
794
  else
732
795
  delete_entry(key, **options)
@@ -752,13 +815,93 @@ module ActiveSupport
752
815
  end
753
816
 
754
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
+
755
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
+
756
854
  def load(payload)
757
- 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)
758
871
  end
872
+ end
873
+
874
+ module Rails61Coder
875
+ include Loader
876
+ extend self
759
877
 
760
878
  def dump(entry)
761
- 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
762
905
  end
763
906
  end
764
907
  end
@@ -771,19 +914,22 @@ module ActiveSupport
771
914
  # Since cache entries in most instances will be serialized, the internals of this class are highly optimized
772
915
  # using short instance variable names that are lazily defined.
773
916
  class Entry # :nodoc:
774
- 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
775
922
 
776
- DEFAULT_COMPRESS_LIMIT = 1.kilobyte
923
+ attr_reader :version
777
924
 
778
925
  # Creates a new cache entry for the specified value. Options supported are
779
- # +:compress+, +:compress_threshold+, +:version+ and +:expires_in+.
780
- 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, **)
781
928
  @value = value
782
929
  @version = version
783
- @created_at = Time.now.to_f
784
- @expires_in = expires_in && expires_in.to_f
785
-
786
- 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
787
933
  end
788
934
 
789
935
  def value
@@ -825,6 +971,38 @@ module ActiveSupport
825
971
  end
826
972
  end
827
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
+
828
1006
  # Duplicates the value in a class. This is used by cache implementations that don't natively
829
1007
  # serialize entries to protect against accidental cache modifications.
830
1008
  def dup_value!
@@ -837,33 +1015,13 @@ module ActiveSupport
837
1015
  end
838
1016
  end
839
1017
 
840
- private
841
- def compress!(compress_threshold)
842
- case @value
843
- when nil, true, false, Numeric
844
- uncompressed_size = 0
845
- when String
846
- uncompressed_size = @value.bytesize
847
- else
848
- serialized = Marshal.dump(@value)
849
- uncompressed_size = serialized.bytesize
850
- end
851
-
852
- if uncompressed_size >= compress_threshold
853
- serialized ||= Marshal.dump(@value)
854
- compressed = Zlib::Deflate.deflate(serialized)
855
-
856
- if compressed.bytesize < uncompressed_size
857
- @value = compressed
858
- @compressed = true
859
- end
860
- end
861
- end
862
-
863
- def compressed?
864
- defined?(@compressed)
865
- end
1018
+ def pack
1019
+ members = [value, expires_at, version]
1020
+ members.pop while !members.empty? && members.last.nil?
1021
+ members
1022
+ end
866
1023
 
1024
+ private
867
1025
  def uncompress(value)
868
1026
  Marshal.load(Zlib::Inflate.inflate(value))
869
1027
  end