activesupport 6.0.4.4 → 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 (212) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +257 -532
  3. data/MIT-LICENSE +1 -1
  4. data/lib/active_support/actionable_error.rb +1 -1
  5. data/lib/active_support/array_inquirer.rb +2 -2
  6. data/lib/active_support/backtrace_cleaner.rb +5 -5
  7. data/lib/active_support/benchmarkable.rb +3 -3
  8. data/lib/active_support/cache/file_store.rb +16 -10
  9. data/lib/active_support/cache/mem_cache_store.rb +163 -42
  10. data/lib/active_support/cache/memory_store.rb +57 -29
  11. data/lib/active_support/cache/null_store.rb +10 -2
  12. data/lib/active_support/cache/redis_cache_store.rb +79 -98
  13. data/lib/active_support/cache/strategy/local_cache.rb +49 -57
  14. data/lib/active_support/cache.rb +378 -179
  15. data/lib/active_support/callbacks.rb +230 -122
  16. data/lib/active_support/code_generator.rb +65 -0
  17. data/lib/active_support/concern.rb +49 -5
  18. data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +2 -4
  19. data/lib/active_support/concurrency/share_lock.rb +2 -2
  20. data/lib/active_support/configurable.rb +9 -6
  21. data/lib/active_support/configuration_file.rb +51 -0
  22. data/lib/active_support/core_ext/array/access.rb +1 -5
  23. data/lib/active_support/core_ext/array/conversions.rb +13 -12
  24. data/lib/active_support/core_ext/array/deprecated_conversions.rb +25 -0
  25. data/lib/active_support/core_ext/array/grouping.rb +6 -6
  26. data/lib/active_support/core_ext/array/inquiry.rb +2 -2
  27. data/lib/active_support/core_ext/array.rb +1 -0
  28. data/lib/active_support/core_ext/benchmark.rb +2 -2
  29. data/lib/active_support/core_ext/big_decimal/conversions.rb +1 -1
  30. data/lib/active_support/core_ext/class/attribute.rb +34 -44
  31. data/lib/active_support/core_ext/class/subclasses.rb +9 -22
  32. data/lib/active_support/core_ext/date/blank.rb +1 -1
  33. data/lib/active_support/core_ext/date/calculations.rb +9 -9
  34. data/lib/active_support/core_ext/date/conversions.rb +16 -15
  35. data/lib/active_support/core_ext/date/deprecated_conversions.rb +26 -0
  36. data/lib/active_support/core_ext/date.rb +1 -0
  37. data/lib/active_support/core_ext/date_and_time/calculations.rb +17 -4
  38. data/lib/active_support/core_ext/date_and_time/compatibility.rb +15 -0
  39. data/lib/active_support/core_ext/date_time/blank.rb +1 -1
  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 +22 -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 +164 -23
  45. data/lib/active_support/core_ext/file/atomic.rb +3 -1
  46. data/lib/active_support/core_ext/hash/conversions.rb +2 -3
  47. data/lib/active_support/core_ext/hash/deep_transform_values.rb +1 -1
  48. data/lib/active_support/core_ext/hash/indifferent_access.rb +3 -3
  49. data/lib/active_support/core_ext/hash/keys.rb +2 -2
  50. data/lib/active_support/core_ext/hash/slice.rb +3 -2
  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/load_error.rb +1 -1
  54. data/lib/active_support/core_ext/module/attr_internal.rb +2 -2
  55. data/lib/active_support/core_ext/module/attribute_accessors.rb +25 -29
  56. data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +26 -13
  57. data/lib/active_support/core_ext/module/concerning.rb +8 -2
  58. data/lib/active_support/core_ext/module/delegation.rb +40 -36
  59. data/lib/active_support/core_ext/module/introspection.rb +1 -25
  60. data/lib/active_support/core_ext/name_error.rb +23 -2
  61. data/lib/active_support/core_ext/numeric/conversions.rb +80 -73
  62. data/lib/active_support/core_ext/numeric/deprecated_conversions.rb +60 -0
  63. data/lib/active_support/core_ext/numeric.rb +1 -0
  64. data/lib/active_support/core_ext/object/acts_like.rb +29 -5
  65. data/lib/active_support/core_ext/object/blank.rb +2 -2
  66. data/lib/active_support/core_ext/object/deep_dup.rb +1 -1
  67. data/lib/active_support/core_ext/object/duplicable.rb +11 -0
  68. data/lib/active_support/core_ext/object/json.rb +42 -26
  69. data/lib/active_support/core_ext/object/to_query.rb +2 -2
  70. data/lib/active_support/core_ext/object/try.rb +20 -20
  71. data/lib/active_support/core_ext/object/with_options.rb +20 -1
  72. data/lib/active_support/core_ext/pathname/existence.rb +21 -0
  73. data/lib/active_support/core_ext/pathname.rb +3 -0
  74. data/lib/active_support/core_ext/range/compare_range.rb +6 -25
  75. data/lib/active_support/core_ext/range/conversions.rb +8 -8
  76. data/lib/active_support/core_ext/range/deprecated_conversions.rb +26 -0
  77. data/lib/active_support/core_ext/range/each.rb +1 -1
  78. data/lib/active_support/core_ext/range/include_time_with_zone.rb +4 -20
  79. data/lib/active_support/core_ext/range/overlaps.rb +1 -1
  80. data/lib/active_support/core_ext/range.rb +1 -1
  81. data/lib/active_support/core_ext/regexp.rb +8 -1
  82. data/lib/active_support/core_ext/securerandom.rb +1 -1
  83. data/lib/active_support/core_ext/string/access.rb +5 -24
  84. data/lib/active_support/core_ext/string/conversions.rb +3 -2
  85. data/lib/active_support/core_ext/string/filters.rb +1 -1
  86. data/lib/active_support/core_ext/string/inflections.rb +39 -5
  87. data/lib/active_support/core_ext/string/inquiry.rb +2 -1
  88. data/lib/active_support/core_ext/string/multibyte.rb +2 -2
  89. data/lib/active_support/core_ext/string/output_safety.rb +92 -41
  90. data/lib/active_support/core_ext/string/starts_ends_with.rb +2 -2
  91. data/lib/active_support/core_ext/symbol/starts_ends_with.rb +6 -0
  92. data/lib/active_support/core_ext/symbol.rb +3 -0
  93. data/lib/active_support/core_ext/time/calculations.rb +25 -7
  94. data/lib/active_support/core_ext/time/conversions.rb +15 -12
  95. data/lib/active_support/core_ext/time/deprecated_conversions.rb +22 -0
  96. data/lib/active_support/core_ext/time/zones.rb +7 -22
  97. data/lib/active_support/core_ext/time.rb +1 -0
  98. data/lib/active_support/core_ext/uri.rb +3 -23
  99. data/lib/active_support/core_ext.rb +2 -1
  100. data/lib/active_support/current_attributes/test_helper.rb +13 -0
  101. data/lib/active_support/current_attributes.rb +39 -16
  102. data/lib/active_support/dependencies/interlock.rb +10 -18
  103. data/lib/active_support/dependencies/require_dependency.rb +28 -0
  104. data/lib/active_support/dependencies.rb +58 -769
  105. data/lib/active_support/deprecation/behaviors.rb +23 -7
  106. data/lib/active_support/deprecation/disallowed.rb +56 -0
  107. data/lib/active_support/deprecation/instance_delegator.rb +0 -1
  108. data/lib/active_support/deprecation/method_wrappers.rb +6 -5
  109. data/lib/active_support/deprecation/proxy_wrappers.rb +4 -4
  110. data/lib/active_support/deprecation/reporting.rb +50 -7
  111. data/lib/active_support/deprecation.rb +7 -2
  112. data/lib/active_support/descendants_tracker.rb +174 -64
  113. data/lib/active_support/digest.rb +5 -3
  114. data/lib/active_support/duration/iso8601_parser.rb +3 -3
  115. data/lib/active_support/duration/iso8601_serializer.rb +24 -10
  116. data/lib/active_support/duration.rb +134 -55
  117. data/lib/active_support/encrypted_configuration.rb +13 -2
  118. data/lib/active_support/encrypted_file.rb +32 -3
  119. data/lib/active_support/environment_inquirer.rb +20 -0
  120. data/lib/active_support/error_reporter.rb +117 -0
  121. data/lib/active_support/evented_file_update_checker.rb +72 -138
  122. data/lib/active_support/execution_context/test_helper.rb +13 -0
  123. data/lib/active_support/execution_context.rb +53 -0
  124. data/lib/active_support/execution_wrapper.rb +43 -21
  125. data/lib/active_support/executor/test_helper.rb +7 -0
  126. data/lib/active_support/fork_tracker.rb +71 -0
  127. data/lib/active_support/gem_version.rb +3 -3
  128. data/lib/active_support/hash_with_indifferent_access.rb +51 -25
  129. data/lib/active_support/html_safe_translation.rb +43 -0
  130. data/lib/active_support/i18n.rb +1 -0
  131. data/lib/active_support/i18n_railtie.rb +14 -19
  132. data/lib/active_support/inflector/inflections.rb +24 -9
  133. data/lib/active_support/inflector/methods.rb +29 -49
  134. data/lib/active_support/inflector/transliterate.rb +5 -5
  135. data/lib/active_support/isolated_execution_state.rb +72 -0
  136. data/lib/active_support/json/decoding.rb +4 -4
  137. data/lib/active_support/json/encoding.rb +8 -4
  138. data/lib/active_support/key_generator.rb +23 -6
  139. data/lib/active_support/lazy_load_hooks.rb +28 -4
  140. data/lib/active_support/locale/en.yml +8 -4
  141. data/lib/active_support/log_subscriber/test_helper.rb +2 -2
  142. data/lib/active_support/log_subscriber.rb +23 -5
  143. data/lib/active_support/logger.rb +1 -1
  144. data/lib/active_support/logger_silence.rb +2 -26
  145. data/lib/active_support/logger_thread_safe_level.rb +34 -21
  146. data/lib/active_support/message_encryptor.rb +16 -13
  147. data/lib/active_support/message_verifier.rb +50 -18
  148. data/lib/active_support/messages/metadata.rb +2 -2
  149. data/lib/active_support/messages/rotation_configuration.rb +2 -1
  150. data/lib/active_support/messages/rotator.rb +6 -5
  151. data/lib/active_support/multibyte/chars.rb +13 -52
  152. data/lib/active_support/multibyte/unicode.rb +1 -87
  153. data/lib/active_support/multibyte.rb +1 -1
  154. data/lib/active_support/notifications/fanout.rb +110 -69
  155. data/lib/active_support/notifications/instrumenter.rb +37 -29
  156. data/lib/active_support/notifications.rb +55 -28
  157. data/lib/active_support/number_helper/number_converter.rb +2 -4
  158. data/lib/active_support/number_helper/number_to_currency_converter.rb +11 -6
  159. data/lib/active_support/number_helper/number_to_delimited_converter.rb +1 -1
  160. data/lib/active_support/number_helper/number_to_human_converter.rb +1 -1
  161. data/lib/active_support/number_helper/number_to_human_size_converter.rb +2 -2
  162. data/lib/active_support/number_helper/number_to_phone_converter.rb +1 -1
  163. data/lib/active_support/number_helper/number_to_rounded_converter.rb +9 -5
  164. data/lib/active_support/number_helper/rounding_helper.rb +12 -32
  165. data/lib/active_support/number_helper.rb +29 -16
  166. data/lib/active_support/option_merger.rb +11 -18
  167. data/lib/active_support/ordered_hash.rb +1 -1
  168. data/lib/active_support/ordered_options.rb +9 -3
  169. data/lib/active_support/parameter_filter.rb +21 -11
  170. data/lib/active_support/per_thread_registry.rb +6 -1
  171. data/lib/active_support/rails.rb +1 -4
  172. data/lib/active_support/railtie.rb +77 -5
  173. data/lib/active_support/reloader.rb +1 -1
  174. data/lib/active_support/rescuable.rb +16 -16
  175. data/lib/active_support/ruby_features.rb +7 -0
  176. data/lib/active_support/secure_compare_rotator.rb +51 -0
  177. data/lib/active_support/security_utils.rb +19 -12
  178. data/lib/active_support/string_inquirer.rb +2 -2
  179. data/lib/active_support/subscriber.rb +19 -25
  180. data/lib/active_support/tagged_logging.rb +31 -6
  181. data/lib/active_support/test_case.rb +13 -21
  182. data/lib/active_support/testing/assertions.rb +50 -13
  183. data/lib/active_support/testing/deprecation.rb +52 -1
  184. data/lib/active_support/testing/isolation.rb +2 -2
  185. data/lib/active_support/testing/method_call_assertions.rb +5 -5
  186. data/lib/active_support/testing/parallelization/server.rb +82 -0
  187. data/lib/active_support/testing/parallelization/worker.rb +103 -0
  188. data/lib/active_support/testing/parallelization.rb +16 -95
  189. data/lib/active_support/testing/parallelize_executor.rb +76 -0
  190. data/lib/active_support/testing/stream.rb +3 -5
  191. data/lib/active_support/testing/tagged_logging.rb +1 -1
  192. data/lib/active_support/testing/time_helpers.rb +53 -5
  193. data/lib/active_support/time_with_zone.rb +126 -62
  194. data/lib/active_support/values/time_zone.rb +54 -23
  195. data/lib/active_support/version.rb +1 -1
  196. data/lib/active_support/xml_mini/jdom.rb +1 -1
  197. data/lib/active_support/xml_mini/libxml.rb +5 -5
  198. data/lib/active_support/xml_mini/libxmlsax.rb +1 -1
  199. data/lib/active_support/xml_mini/nokogiri.rb +4 -4
  200. data/lib/active_support/xml_mini/nokogirisax.rb +1 -1
  201. data/lib/active_support/xml_mini/rexml.rb +9 -2
  202. data/lib/active_support/xml_mini.rb +5 -4
  203. data/lib/active_support.rb +29 -1
  204. metadata +46 -45
  205. data/lib/active_support/core_ext/array/prepend_and_append.rb +0 -5
  206. data/lib/active_support/core_ext/hash/compact.rb +0 -5
  207. data/lib/active_support/core_ext/hash/transform_values.rb +0 -5
  208. data/lib/active_support/core_ext/marshal.rb +0 -24
  209. data/lib/active_support/core_ext/module/reachable.rb +0 -6
  210. data/lib/active_support/core_ext/numeric/inquiry.rb +0 -5
  211. data/lib/active_support/core_ext/range/include_range.rb +0 -9
  212. data/lib/active_support/dependencies/zeitwerk_integration.rb +0 -117
@@ -3,10 +3,12 @@
3
3
  require "zlib"
4
4
  require "active_support/core_ext/array/extract_options"
5
5
  require "active_support/core_ext/array/wrap"
6
+ require "active_support/core_ext/enumerable"
6
7
  require "active_support/core_ext/module/attribute_accessors"
7
8
  require "active_support/core_ext/numeric/bytes"
8
9
  require "active_support/core_ext/numeric/time"
9
10
  require "active_support/core_ext/object/to_param"
11
+ require "active_support/core_ext/object/try"
10
12
  require "active_support/core_ext/string/inflections"
11
13
 
12
14
  module ActiveSupport
@@ -20,13 +22,24 @@ module ActiveSupport
20
22
 
21
23
  # These options mean something to all cache implementations. Individual cache
22
24
  # implementations may support additional options.
23
- UNIVERSAL_OPTIONS = [:namespace, :compress, :compress_threshold, :expires_in, :race_condition_ttl]
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
24
33
 
25
34
  module Strategy
26
35
  autoload :LocalCache, "active_support/cache/strategy/local_cache"
27
36
  end
28
37
 
38
+ @format_version = 6.1
39
+
29
40
  class << self
41
+ attr_accessor :format_version
42
+
30
43
  # Creates a new Store object according to the given options.
31
44
  #
32
45
  # If no arguments are passed to this method, then a new
@@ -56,7 +69,13 @@ module ActiveSupport
56
69
  case store
57
70
  when Symbol
58
71
  options = parameters.extract_options!
59
- 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
60
79
  when Array
61
80
  lookup_store(*store)
62
81
  when nil
@@ -79,7 +98,7 @@ module ActiveSupport
79
98
  #
80
99
  # The +key+ argument can also respond to +cache_key+ or +to_param+.
81
100
  def expand_cache_key(key, namespace = nil)
82
- expanded_cache_key = (namespace ? "#{namespace}/" : "").dup
101
+ expanded_cache_key = namespace ? +"#{namespace}/" : +""
83
102
 
84
103
  if prefix = ENV["RAILS_CACHE_ID"] || ENV["RAILS_APP_VERSION"]
85
104
  expanded_cache_key << "#{prefix}/"
@@ -120,9 +139,10 @@ module ActiveSupport
120
139
  # popular cache store for large production websites.
121
140
  #
122
141
  # Some implementations may not support all methods beyond the basic cache
123
- # methods of +fetch+, +write+, +read+, +exist?+, and +delete+.
142
+ # methods of #fetch, #write, #read, #exist?, and #delete.
124
143
  #
125
- # 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.
126
146
  #
127
147
  # cache = ActiveSupport::Cache::MemoryStore.new
128
148
  #
@@ -130,6 +150,8 @@ module ActiveSupport
130
150
  # cache.write('city', "Duckburgh")
131
151
  # cache.read('city') # => "Duckburgh"
132
152
  #
153
+ # cache.write('not serializable', Proc.new {}) # => TypeError
154
+ #
133
155
  # Keys are always translated into Strings and are case sensitive. When an
134
156
  # object is specified as a key and has a +cache_key+ method defined, this
135
157
  # method will be called to define the key. Otherwise, the +to_param+
@@ -150,11 +172,6 @@ module ActiveSupport
150
172
  # cache.namespace = -> { @last_mod_time } # Set the namespace to a variable
151
173
  # @last_mod_time = Time.now # Invalidate the entire cache by changing namespace
152
174
  #
153
- # Cached data larger than 1kB are compressed by default. To turn off
154
- # compression, pass <tt>compress: false</tt> to the initializer or to
155
- # individual +fetch+ or +write+ method calls. The 1kB compression
156
- # threshold is configurable with the <tt>:compress_threshold</tt> option,
157
- # specified in bytes.
158
175
  class Store
159
176
  cattr_accessor :logger, instance_writer: true
160
177
 
@@ -178,11 +195,26 @@ module ActiveSupport
178
195
  end
179
196
  end
180
197
 
181
- # Creates a new cache. The options will be passed to any write method calls
182
- # except for <tt>:namespace</tt> which can be used to set the global
183
- # 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.
184
211
  def initialize(options = nil)
185
- @options = options ? options.dup : {}
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)
186
218
  end
187
219
 
188
220
  # Silences the logger.
@@ -217,101 +249,75 @@ module ActiveSupport
217
249
  # end
218
250
  # cache.fetch('city') # => "Duckburgh"
219
251
  #
220
- # You may also specify additional options via the +options+ argument.
221
- # Setting <tt>force: true</tt> forces a cache "miss," meaning we treat
222
- # the cache value as missing even if it's present. Passing a block is
223
- # required when +force+ is true so this always results in a cache write.
252
+ # ==== Options
224
253
  #
225
- # cache.write('today', 'Monday')
226
- # cache.fetch('today', force: true) { 'Tuesday' } # => 'Tuesday'
227
- # cache.fetch('today', force: true) # => ArgumentError
228
- #
229
- # The +:force+ option is useful when you're calling some other method to
230
- # ask whether you should force a cache write. Otherwise, it's clearer to
231
- # just call <tt>Cache#write</tt>.
232
- #
233
- # Setting <tt>skip_nil: true</tt> will not cache nil result:
234
- #
235
- # cache.fetch('foo') { nil }
236
- # cache.fetch('bar', skip_nil: true) { nil }
237
- # cache.exist?('foo') # => true
238
- # cache.exist?('bar') # => false
239
- #
240
- #
241
- # Setting <tt>compress: false</tt> disables compression of the cache entry.
242
- #
243
- # Setting <tt>:expires_in</tt> will set an expiration time on the cache.
244
- # All caches support auto-expiring content after a specified number of
245
- # seconds. This value can be specified as an option to the constructor
246
- # (in which case all entries will be affected), or it can be supplied to
247
- # the +fetch+ or +write+ method to effect just one entry.
248
- #
249
- # cache = ActiveSupport::Cache::MemoryStore.new(expires_in: 5.minutes)
250
- # cache.write(key, value, expires_in: 1.minute) # Set a lower value for one entry
251
- #
252
- # Setting <tt>:version</tt> verifies the cache stored under <tt>name</tt>
253
- # is of the same version. nil is returned on mismatches despite contents.
254
- # This feature is used to support recyclable cache keys.
255
- #
256
- # Setting <tt>:race_condition_ttl</tt> is very useful in situations where
257
- # a cache entry is used very frequently and is under heavy load. If a
258
- # cache expires and due to heavy load several different processes will try
259
- # to read data natively and then they all will try to write to cache. To
260
- # avoid that case the first process to find an expired cache entry will
261
- # bump the cache expiration time by the value set in <tt>:race_condition_ttl</tt>.
262
- # Yes, this process is extending the time for a stale value by another few
263
- # seconds. Because of extended life of the previous cache, other processes
264
- # will continue to use slightly stale data for a just a bit longer. In the
265
- # meantime that first process will go ahead and will write into cache the
266
- # new value. After that all the processes will start getting the new value.
267
- # The key is to keep <tt>:race_condition_ttl</tt> small.
268
- #
269
- # If the process regenerating the entry errors out, the entry will be
270
- # regenerated after the specified number of seconds. Also note that the
271
- # life of stale cache is extended only if it expired recently. Otherwise
272
- # a new value is generated and <tt>:race_condition_ttl</tt> does not play
273
- # any role.
274
- #
275
- # # Set all values to expire after one minute.
276
- # cache = ActiveSupport::Cache::MemoryStore.new(expires_in: 1.minute)
277
- #
278
- # cache.write('foo', 'original value')
279
- # val_1 = nil
280
- # val_2 = nil
281
- # sleep 60
282
- #
283
- # Thread.new do
284
- # val_1 = cache.fetch('foo', race_condition_ttl: 10.seconds) do
285
- # sleep 1
286
- # 'new value 1'
287
- # end
288
- # 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:
289
257
  #
290
- # Thread.new do
291
- # val_2 = cache.fetch('foo', race_condition_ttl: 10.seconds) do
292
- # 'new value 2'
293
- # end
294
- # 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.
295
261
  #
296
- # cache.fetch('foo') # => "original value"
297
- # sleep 10 # First thread extended the life of cache by another 10 seconds
298
- # cache.fetch('foo') # => "new value 1"
299
- # val_1 # => "new value 1"
300
- # val_2 # => "original value"
262
+ # cache.write('today', 'Monday')
263
+ # cache.fetch('today', force: true) { 'Tuesday' } # => 'Tuesday'
264
+ # cache.fetch('today', force: true) # => ArgumentError
301
265
  #
302
- # Other options will be handled by the specific cache store implementation.
303
- # Internally, #fetch calls #read_entry, and calls #write_entry on a cache
304
- # 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+.
305
269
  #
306
- # For example, MemCacheStore's #write method supports the +:raw+
307
- # option, which tells the memcached server to store all values as strings.
308
- # 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"
309
320
  #
310
- # cache = ActiveSupport::Cache::MemCacheStore.new
311
- # cache.fetch("foo", force: true, raw: true) do
312
- # :bar
313
- # end
314
- # cache.fetch('foo') # => "bar"
315
321
  def fetch(name, options = nil, &block)
316
322
  if block_given?
317
323
  options = merged_options(options)
@@ -319,7 +325,7 @@ module ActiveSupport
319
325
 
320
326
  entry = nil
321
327
  instrument(:read, name, options) do |payload|
322
- cached_entry = read_entry(key, **options) unless options[:force]
328
+ cached_entry = read_entry(key, **options, event: payload) unless options[:force]
323
329
  entry = handle_expired_entry(cached_entry, key, options)
324
330
  entry = nil if entry && entry.mismatched?(normalize_version(name, options))
325
331
  payload[:super_operation] = :fetch if payload
@@ -346,14 +352,20 @@ module ActiveSupport
346
352
  # <tt>:version</tt> options, both of these conditions are applied before
347
353
  # the data is returned.
348
354
  #
349
- # 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.
350
362
  def read(name, options = nil)
351
363
  options = merged_options(options)
352
364
  key = normalize_key(name, options)
353
365
  version = normalize_version(name, options)
354
366
 
355
367
  instrument(:read, name, options) do |payload|
356
- entry = read_entry(key, **options)
368
+ entry = read_entry(key, **options, event: payload)
357
369
 
358
370
  if entry
359
371
  if entry.expired?
@@ -385,7 +397,7 @@ module ActiveSupport
385
397
  options = merged_options(options)
386
398
 
387
399
  instrument :read_multi, names, options do |payload|
388
- read_multi_entries(names, **options).tap do |results|
400
+ read_multi_entries(names, **options, event: payload).tap do |results|
389
401
  payload[:hits] = results.keys
390
402
  end
391
403
  end
@@ -441,8 +453,8 @@ module ActiveSupport
441
453
  instrument :read_multi, names, options do |payload|
442
454
  reads = read_multi_entries(names, **options)
443
455
  writes = {}
444
- ordered = names.each_with_object({}) do |name, hash|
445
- hash[name] = reads.fetch(name) { writes[name] = yield(name) }
456
+ ordered = names.index_with do |name|
457
+ reads.fetch(name) { writes[name] = yield(name) }
446
458
  end
447
459
 
448
460
  payload[:hits] = reads.keys
@@ -454,9 +466,39 @@ module ActiveSupport
454
466
  end
455
467
  end
456
468
 
457
- # 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.
458
471
  #
459
- # 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.
460
502
  def write(name, value, options = nil)
461
503
  options = merged_options(options)
462
504
 
@@ -477,18 +519,34 @@ module ActiveSupport
477
519
  end
478
520
  end
479
521
 
522
+ # Deletes multiple entries in the cache.
523
+ #
524
+ # Options are passed to the underlying cache implementation.
525
+ def delete_multi(names, options = nil)
526
+ options = merged_options(options)
527
+ names.map! { |key| normalize_key(key, options) }
528
+
529
+ instrument :delete_multi, names do
530
+ delete_multi_entries(names, **options)
531
+ end
532
+ end
533
+
480
534
  # Returns +true+ if the cache contains an entry for the given key.
481
535
  #
482
536
  # Options are passed to the underlying cache implementation.
483
537
  def exist?(name, options = nil)
484
538
  options = merged_options(options)
485
539
 
486
- instrument(:exist?, name) do
487
- entry = read_entry(normalize_key(name, options), **options)
540
+ instrument(:exist?, name) do |payload|
541
+ entry = read_entry(normalize_key(name, options), **options, event: payload)
488
542
  (entry && !entry.expired? && !entry.mismatched?(normalize_version(name, options))) || false
489
543
  end
490
544
  end
491
545
 
546
+ def new_entry(value, options = nil) # :nodoc:
547
+ Entry.new(value, **merged_options(options))
548
+ end
549
+
492
550
  # Deletes all entries with keys matching the pattern.
493
551
  #
494
552
  # Options are passed to the underlying cache implementation.
@@ -516,7 +574,7 @@ module ActiveSupport
516
574
  raise NotImplementedError.new("#{self.class.name} does not support decrement")
517
575
  end
518
576
 
519
- # Cleanups the cache by removing expired entries.
577
+ # Cleans up the cache by removing expired entries.
520
578
  #
521
579
  # Options are passed to the underlying cache implementation.
522
580
  #
@@ -536,6 +594,10 @@ module ActiveSupport
536
594
  end
537
595
 
538
596
  private
597
+ def default_coder
598
+ Coders[Cache.format_version]
599
+ end
600
+
539
601
  # Adds the namespace defined in the options to a pattern designed to
540
602
  # match keys. Implementations that support delete_matched should call
541
603
  # this method to translate a pattern that matches names into one that
@@ -567,26 +629,36 @@ module ActiveSupport
567
629
  raise NotImplementedError.new
568
630
  end
569
631
 
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
639
+ end
640
+
641
+ def deserialize_entry(payload)
642
+ payload.nil? ? nil : @coder.load(payload)
643
+ end
644
+
570
645
  # Reads multiple entries from the cache implementation. Subclasses MAY
571
646
  # implement this method.
572
647
  def read_multi_entries(names, **options)
573
- results = {}
574
- names.each do |name|
575
- key = normalize_key(name, options)
648
+ names.each_with_object({}) do |name, results|
649
+ key = normalize_key(name, options)
650
+ entry = read_entry(key, **options)
651
+
652
+ next unless entry
653
+
576
654
  version = normalize_version(name, options)
577
- entry = read_entry(key, **options)
578
-
579
- if entry
580
- if entry.expired?
581
- delete_entry(key, **options)
582
- elsif entry.mismatched?(version)
583
- # Skip mismatched versions
584
- else
585
- results[name] = entry.value
586
- end
655
+
656
+ if entry.expired?
657
+ delete_entry(key, **options)
658
+ elsif !entry.mismatched?(version)
659
+ results[name] = entry.value
587
660
  end
588
661
  end
589
- results
590
662
  end
591
663
 
592
664
  # Writes multiple entries to the cache implementation. Subclasses MAY
@@ -603,9 +675,16 @@ module ActiveSupport
603
675
  raise NotImplementedError.new
604
676
  end
605
677
 
678
+ # Deletes multiples entries in the cache implementation. Subclasses MAY
679
+ # implement this method.
680
+ def delete_multi_entries(entries, **options)
681
+ entries.count { |key| delete_entry(key, **options) }
682
+ end
683
+
606
684
  # Merges the default options with ones specific to a method call.
607
685
  def merged_options(call_options)
608
686
  if call_options
687
+ call_options = normalize_options(call_options)
609
688
  if options.empty?
610
689
  call_options
611
690
  else
@@ -616,6 +695,18 @@ module ActiveSupport
616
695
  end
617
696
  end
618
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
+
619
710
  # Expands and namespaces the cache key. May be overridden by
620
711
  # cache stores to do additional normalization.
621
712
  def normalize_key(key, options = nil)
@@ -639,6 +730,10 @@ module ActiveSupport
639
730
  namespace = namespace.call
640
731
  end
641
732
 
733
+ if key && key.encoding != Encoding::UTF_8
734
+ key = key.dup.force_encoding(Encoding::UTF_8)
735
+ end
736
+
642
737
  if namespace
643
738
  "#{namespace}:#{key}"
644
739
  else
@@ -655,15 +750,15 @@ module ActiveSupport
655
750
  case key
656
751
  when Array
657
752
  if key.size > 1
658
- key = key.collect { |element| expanded_key(element) }
753
+ key.collect { |element| expanded_key(element) }
659
754
  else
660
- key = expanded_key(key.first)
755
+ expanded_key(key.first)
661
756
  end
662
757
  when Hash
663
- key = key.sort_by { |k, _| k.to_s }.collect { |k, v| "#{k}=#{v}" }
664
- end
665
-
666
- key.to_param
758
+ key.collect { |k, v| "#{k}=#{v}" }.sort!
759
+ else
760
+ key
761
+ end.to_param
667
762
  end
668
763
 
669
764
  def normalize_version(key, options = nil)
@@ -673,31 +768,28 @@ module ActiveSupport
673
768
  def expanded_version(key)
674
769
  case
675
770
  when key.respond_to?(:cache_version) then key.cache_version.to_param
676
- when key.is_a?(Array) then key.map { |element| expanded_version(element) }.compact.to_param
771
+ when key.is_a?(Array) then key.map { |element| expanded_version(element) }.tap(&:compact!).to_param
677
772
  when key.respond_to?(:to_a) then expanded_version(key.to_a)
678
773
  end
679
774
  end
680
775
 
681
776
  def instrument(operation, key, options = nil)
682
- log { "Cache #{operation}: #{normalize_key(key, options)}#{options.blank? ? "" : " (#{options.inspect})"}" }
777
+ if logger && logger.debug? && !silence?
778
+ logger.debug "Cache #{operation}: #{normalize_key(key, options)}#{options.blank? ? "" : " (#{options.inspect})"}"
779
+ end
683
780
 
684
- payload = { key: key }
781
+ payload = { key: key, store: self.class.name }
685
782
  payload.merge!(options) if options.is_a?(Hash)
686
783
  ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload) { yield(payload) }
687
784
  end
688
785
 
689
- def log
690
- return unless logger && logger.debug? && !silence?
691
- logger.debug(yield)
692
- end
693
-
694
786
  def handle_expired_entry(entry, key, options)
695
787
  if entry && entry.expired?
696
788
  race_ttl = options[:race_condition_ttl].to_i
697
789
  if (race_ttl > 0) && (Time.now.to_f - entry.expires_at <= race_ttl)
698
790
  # When an entry has a positive :race_condition_ttl defined, put the stale entry back into the cache
699
791
  # for a brief period while the entry is being recalculated.
700
- entry.expires_at = Time.now + race_ttl
792
+ entry.expires_at = Time.now.to_f + race_ttl
701
793
  write_entry(key, entry, expires_in: race_ttl * 2)
702
794
  else
703
795
  delete_entry(key, **options)
@@ -722,6 +814,98 @@ module ActiveSupport
722
814
  end
723
815
  end
724
816
 
817
+ module NullCoder # :nodoc:
818
+ extend self
819
+
820
+ def dump(entry)
821
+ entry
822
+ end
823
+
824
+ def dump_compressed(entry, threshold)
825
+ entry.compressed(threshold)
826
+ end
827
+
828
+ def load(payload)
829
+ payload
830
+ end
831
+ end
832
+
833
+ module Coders # :nodoc:
834
+ MARK_61 = "\x04\b".b.freeze # The one set by Marshal.
835
+ MARK_70_UNCOMPRESSED = "\x00".b.freeze
836
+ MARK_70_COMPRESSED = "\x01".b.freeze
837
+
838
+ class << self
839
+ def [](version)
840
+ case version
841
+ when 6.1
842
+ Rails61Coder
843
+ when 7.0
844
+ Rails70Coder
845
+ else
846
+ raise ArgumentError, "Unknown ActiveSupport::Cache.format_version: #{Cache.format_version.inspect}"
847
+ end
848
+ end
849
+ end
850
+
851
+ module Loader
852
+ extend self
853
+
854
+ def load(payload)
855
+ if !payload.is_a?(String)
856
+ ActiveSupport::Cache::Store.logger&.warn %{Payload wasn't a string, was #{payload.class.name} - couldn't unmarshal, so returning nil."}
857
+
858
+ return nil
859
+ elsif payload.start_with?(MARK_70_UNCOMPRESSED)
860
+ members = Marshal.load(payload.byteslice(1..-1))
861
+ elsif payload.start_with?(MARK_70_COMPRESSED)
862
+ members = Marshal.load(Zlib::Inflate.inflate(payload.byteslice(1..-1)))
863
+ elsif payload.start_with?(MARK_61)
864
+ return Marshal.load(payload)
865
+ else
866
+ ActiveSupport::Cache::Store.logger&.warn %{Invalid cache prefix: #{payload.byteslice(0).inspect}, expected "\\x00" or "\\x01"}
867
+
868
+ return nil
869
+ end
870
+ Entry.unpack(members)
871
+ end
872
+ end
873
+
874
+ module Rails61Coder
875
+ include Loader
876
+ extend self
877
+
878
+ def dump(entry)
879
+ Marshal.dump(entry)
880
+ end
881
+
882
+ def dump_compressed(entry, threshold)
883
+ Marshal.dump(entry.compressed(threshold))
884
+ end
885
+ end
886
+
887
+ module Rails70Coder
888
+ include Loader
889
+ extend self
890
+
891
+ def dump(entry)
892
+ MARK_70_UNCOMPRESSED + Marshal.dump(entry.pack)
893
+ end
894
+
895
+ def dump_compressed(entry, threshold)
896
+ payload = Marshal.dump(entry.pack)
897
+ if payload.bytesize >= threshold
898
+ compressed_payload = Zlib::Deflate.deflate(payload)
899
+ if compressed_payload.bytesize < payload.bytesize
900
+ return MARK_70_COMPRESSED + compressed_payload
901
+ end
902
+ end
903
+
904
+ MARK_70_UNCOMPRESSED + payload
905
+ end
906
+ end
907
+ end
908
+
725
909
  # This class is used to represent cache entries. Cache entries have a value, an optional
726
910
  # expiration time, and an optional version. The expiration time is used to support the :race_condition_ttl option
727
911
  # on the cache. The version is used to support the :version option on the cache for rejecting
@@ -730,19 +914,22 @@ module ActiveSupport
730
914
  # Since cache entries in most instances will be serialized, the internals of this class are highly optimized
731
915
  # using short instance variable names that are lazily defined.
732
916
  class Entry # :nodoc:
733
- 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
734
922
 
735
- DEFAULT_COMPRESS_LIMIT = 1.kilobyte
923
+ attr_reader :version
736
924
 
737
925
  # Creates a new cache entry for the specified value. Options supported are
738
- # +:compress+, +:compress_threshold+, +:version+ and +:expires_in+.
739
- 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, **)
740
928
  @value = value
741
929
  @version = version
742
- @created_at = Time.now.to_f
743
- @expires_in = expires_in && expires_in.to_f
744
-
745
- 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
746
933
  end
747
934
 
748
935
  def value
@@ -772,8 +959,8 @@ module ActiveSupport
772
959
  end
773
960
 
774
961
  # Returns the size of the cached value. This could be less than
775
- # <tt>value.size</tt> if the data is compressed.
776
- def size
962
+ # <tt>value.bytesize</tt> if the data is compressed.
963
+ def bytesize
777
964
  case value
778
965
  when NilClass
779
966
  0
@@ -784,6 +971,38 @@ module ActiveSupport
784
971
  end
785
972
  end
786
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
+
787
1006
  # Duplicates the value in a class. This is used by cache implementations that don't natively
788
1007
  # serialize entries to protect against accidental cache modifications.
789
1008
  def dup_value!
@@ -796,33 +1015,13 @@ module ActiveSupport
796
1015
  end
797
1016
  end
798
1017
 
799
- private
800
- def compress!(compress_threshold)
801
- case @value
802
- when nil, true, false, Numeric
803
- uncompressed_size = 0
804
- when String
805
- uncompressed_size = @value.bytesize
806
- else
807
- serialized = Marshal.dump(@value)
808
- uncompressed_size = serialized.bytesize
809
- end
810
-
811
- if uncompressed_size >= compress_threshold
812
- serialized ||= Marshal.dump(@value)
813
- compressed = Zlib::Deflate.deflate(serialized)
814
-
815
- if compressed.bytesize < uncompressed_size
816
- @value = compressed
817
- @compressed = true
818
- end
819
- end
820
- end
821
-
822
- def compressed?
823
- defined?(@compressed)
824
- end
1018
+ def pack
1019
+ members = [value, expires_at, version]
1020
+ members.pop while !members.empty? && members.last.nil?
1021
+ members
1022
+ end
825
1023
 
1024
+ private
826
1025
  def uncompress(value)
827
1026
  Marshal.load(Zlib::Inflate.inflate(value))
828
1027
  end