activesupport 6.1.4.1 → 7.0.8.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (185) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +325 -395
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +2 -2
  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 +15 -9
  10. data/lib/active_support/cache/mem_cache_store.rb +148 -37
  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 +68 -85
  14. data/lib/active_support/cache/strategy/local_cache.rb +38 -61
  15. data/lib/active_support/cache.rb +299 -147
  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 +1 -1
  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 +24 -9
  33. data/lib/active_support/core_ext/date/conversions.rb +14 -14
  34. data/lib/active_support/core_ext/date/deprecated_conversions.rb +40 -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/calculations.rb +4 -0
  40. data/lib/active_support/core_ext/date_time/conversions.rb +13 -13
  41. data/lib/active_support/core_ext/date_time/deprecated_conversions.rb +36 -0
  42. data/lib/active_support/core_ext/date_time.rb +1 -0
  43. data/lib/active_support/core_ext/digest/uuid.rb +39 -13
  44. data/lib/active_support/core_ext/enumerable.rb +112 -38
  45. data/lib/active_support/core_ext/file/atomic.rb +3 -1
  46. data/lib/active_support/core_ext/hash/conversions.rb +0 -1
  47. data/lib/active_support/core_ext/hash/deep_transform_values.rb +3 -3
  48. data/lib/active_support/core_ext/hash/indifferent_access.rb +3 -3
  49. data/lib/active_support/core_ext/hash/keys.rb +4 -4
  50. data/lib/active_support/core_ext/integer/inflections.rb +12 -12
  51. data/lib/active_support/core_ext/kernel/reporting.rb +4 -4
  52. data/lib/active_support/core_ext/kernel/singleton_class.rb +1 -1
  53. data/lib/active_support/core_ext/module/attribute_accessors.rb +2 -0
  54. data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +19 -10
  55. data/lib/active_support/core_ext/module/delegation.rb +2 -8
  56. data/lib/active_support/core_ext/name_error.rb +2 -8
  57. data/lib/active_support/core_ext/numeric/conversions.rb +80 -77
  58. data/lib/active_support/core_ext/numeric/deprecated_conversions.rb +60 -0
  59. data/lib/active_support/core_ext/numeric.rb +1 -0
  60. data/lib/active_support/core_ext/object/acts_like.rb +29 -5
  61. data/lib/active_support/core_ext/object/blank.rb +2 -2
  62. data/lib/active_support/core_ext/object/deep_dup.rb +1 -1
  63. data/lib/active_support/core_ext/object/duplicable.rb +15 -4
  64. data/lib/active_support/core_ext/object/json.rb +30 -25
  65. data/lib/active_support/core_ext/object/to_query.rb +2 -4
  66. data/lib/active_support/core_ext/object/try.rb +20 -20
  67. data/lib/active_support/core_ext/object/with_options.rb +21 -2
  68. data/lib/active_support/core_ext/pathname/existence.rb +21 -0
  69. data/lib/active_support/core_ext/pathname.rb +3 -0
  70. data/lib/active_support/core_ext/range/compare_range.rb +0 -25
  71. data/lib/active_support/core_ext/range/conversions.rb +8 -8
  72. data/lib/active_support/core_ext/range/deprecated_conversions.rb +36 -0
  73. data/lib/active_support/core_ext/range/each.rb +1 -1
  74. data/lib/active_support/core_ext/range/include_time_with_zone.rb +3 -26
  75. data/lib/active_support/core_ext/range/overlaps.rb +1 -1
  76. data/lib/active_support/core_ext/range.rb +1 -1
  77. data/lib/active_support/core_ext/securerandom.rb +1 -1
  78. data/lib/active_support/core_ext/string/conversions.rb +2 -2
  79. data/lib/active_support/core_ext/string/filters.rb +1 -1
  80. data/lib/active_support/core_ext/string/inflections.rb +1 -5
  81. data/lib/active_support/core_ext/string/inquiry.rb +1 -1
  82. data/lib/active_support/core_ext/string/output_safety.rb +94 -38
  83. data/lib/active_support/core_ext/symbol/starts_ends_with.rb +0 -8
  84. data/lib/active_support/core_ext/time/calculations.rb +13 -8
  85. data/lib/active_support/core_ext/time/conversions.rb +13 -12
  86. data/lib/active_support/core_ext/time/deprecated_conversions.rb +73 -0
  87. data/lib/active_support/core_ext/time/zones.rb +10 -26
  88. data/lib/active_support/core_ext/time.rb +1 -0
  89. data/lib/active_support/core_ext/uri.rb +3 -27
  90. data/lib/active_support/core_ext.rb +1 -0
  91. data/lib/active_support/current_attributes.rb +31 -14
  92. data/lib/active_support/dependencies/interlock.rb +10 -18
  93. data/lib/active_support/dependencies/require_dependency.rb +28 -0
  94. data/lib/active_support/dependencies.rb +58 -788
  95. data/lib/active_support/deprecation/behaviors.rb +8 -5
  96. data/lib/active_support/deprecation/disallowed.rb +3 -3
  97. data/lib/active_support/deprecation/method_wrappers.rb +3 -3
  98. data/lib/active_support/deprecation/proxy_wrappers.rb +2 -2
  99. data/lib/active_support/deprecation.rb +2 -2
  100. data/lib/active_support/descendants_tracker.rb +174 -68
  101. data/lib/active_support/digest.rb +5 -3
  102. data/lib/active_support/duration/iso8601_parser.rb +3 -3
  103. data/lib/active_support/duration/iso8601_serializer.rb +9 -1
  104. data/lib/active_support/duration.rb +81 -51
  105. data/lib/active_support/encrypted_configuration.rb +45 -3
  106. data/lib/active_support/encrypted_file.rb +21 -10
  107. data/lib/active_support/environment_inquirer.rb +1 -1
  108. data/lib/active_support/error_reporter.rb +117 -0
  109. data/lib/active_support/evented_file_update_checker.rb +20 -7
  110. data/lib/active_support/execution_context/test_helper.rb +13 -0
  111. data/lib/active_support/execution_context.rb +53 -0
  112. data/lib/active_support/execution_wrapper.rb +43 -21
  113. data/lib/active_support/executor/test_helper.rb +7 -0
  114. data/lib/active_support/fork_tracker.rb +19 -12
  115. data/lib/active_support/gem_version.rb +5 -5
  116. data/lib/active_support/hash_with_indifferent_access.rb +3 -1
  117. data/lib/active_support/html_safe_translation.rb +43 -0
  118. data/lib/active_support/i18n.rb +1 -0
  119. data/lib/active_support/i18n_railtie.rb +1 -1
  120. data/lib/active_support/inflector/inflections.rb +23 -7
  121. data/lib/active_support/inflector/methods.rb +29 -55
  122. data/lib/active_support/inflector/transliterate.rb +1 -1
  123. data/lib/active_support/isolated_execution_state.rb +72 -0
  124. data/lib/active_support/json/encoding.rb +3 -3
  125. data/lib/active_support/key_generator.rb +22 -5
  126. data/lib/active_support/lazy_load_hooks.rb +28 -4
  127. data/lib/active_support/locale/en.yml +1 -1
  128. data/lib/active_support/log_subscriber/test_helper.rb +2 -2
  129. data/lib/active_support/log_subscriber.rb +15 -5
  130. data/lib/active_support/logger_thread_safe_level.rb +4 -13
  131. data/lib/active_support/message_encryptor.rb +12 -6
  132. data/lib/active_support/message_verifier.rb +46 -14
  133. data/lib/active_support/messages/metadata.rb +2 -2
  134. data/lib/active_support/multibyte/chars.rb +10 -11
  135. data/lib/active_support/multibyte/unicode.rb +0 -12
  136. data/lib/active_support/multibyte.rb +1 -1
  137. data/lib/active_support/notifications/fanout.rb +91 -65
  138. data/lib/active_support/notifications/instrumenter.rb +32 -15
  139. data/lib/active_support/notifications.rb +23 -23
  140. data/lib/active_support/number_helper/number_converter.rb +1 -3
  141. data/lib/active_support/number_helper/number_to_currency_converter.rb +11 -6
  142. data/lib/active_support/number_helper/number_to_delimited_converter.rb +1 -1
  143. data/lib/active_support/number_helper/number_to_human_size_converter.rb +1 -1
  144. data/lib/active_support/number_helper/number_to_phone_converter.rb +1 -1
  145. data/lib/active_support/number_helper/rounding_helper.rb +1 -5
  146. data/lib/active_support/number_helper.rb +4 -5
  147. data/lib/active_support/option_merger.rb +10 -18
  148. data/lib/active_support/ordered_hash.rb +1 -1
  149. data/lib/active_support/ordered_options.rb +1 -1
  150. data/lib/active_support/parameter_filter.rb +20 -11
  151. data/lib/active_support/per_thread_registry.rb +5 -0
  152. data/lib/active_support/railtie.rb +69 -19
  153. data/lib/active_support/reloader.rb +1 -1
  154. data/lib/active_support/rescuable.rb +12 -12
  155. data/lib/active_support/ruby_features.rb +7 -0
  156. data/lib/active_support/secure_compare_rotator.rb +2 -2
  157. data/lib/active_support/string_inquirer.rb +0 -2
  158. data/lib/active_support/subscriber.rb +7 -18
  159. data/lib/active_support/tagged_logging.rb +2 -2
  160. data/lib/active_support/test_case.rb +13 -21
  161. data/lib/active_support/testing/assertions.rb +36 -6
  162. data/lib/active_support/testing/deprecation.rb +52 -1
  163. data/lib/active_support/testing/isolation.rb +30 -29
  164. data/lib/active_support/testing/method_call_assertions.rb +5 -5
  165. data/lib/active_support/testing/parallelization/server.rb +4 -0
  166. data/lib/active_support/testing/parallelization/worker.rb +3 -0
  167. data/lib/active_support/testing/parallelization.rb +4 -0
  168. data/lib/active_support/testing/parallelize_executor.rb +76 -0
  169. data/lib/active_support/testing/stream.rb +3 -5
  170. data/lib/active_support/testing/tagged_logging.rb +1 -1
  171. data/lib/active_support/testing/time_helpers.rb +13 -2
  172. data/lib/active_support/time_with_zone.rb +43 -22
  173. data/lib/active_support/values/time_zone.rb +35 -14
  174. data/lib/active_support/version.rb +1 -1
  175. data/lib/active_support/xml_mini/jdom.rb +1 -1
  176. data/lib/active_support/xml_mini/libxml.rb +5 -5
  177. data/lib/active_support/xml_mini/libxmlsax.rb +1 -1
  178. data/lib/active_support/xml_mini/nokogiri.rb +4 -4
  179. data/lib/active_support/xml_mini/nokogirisax.rb +1 -1
  180. data/lib/active_support/xml_mini/rexml.rb +1 -1
  181. data/lib/active_support/xml_mini.rb +5 -4
  182. data/lib/active_support.rb +17 -1
  183. metadata +26 -23
  184. data/lib/active_support/core_ext/marshal.rb +0 -26
  185. 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
@@ -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