activesupport 7.0.0 → 7.2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (211) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +156 -255
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +6 -6
  5. data/lib/active_support/actionable_error.rb +3 -1
  6. data/lib/active_support/array_inquirer.rb +3 -1
  7. data/lib/active_support/backtrace_cleaner.rb +41 -9
  8. data/lib/active_support/benchmarkable.rb +1 -0
  9. data/lib/active_support/broadcast_logger.rb +251 -0
  10. data/lib/active_support/builder.rb +1 -1
  11. data/lib/active_support/cache/coder.rb +153 -0
  12. data/lib/active_support/cache/entry.rb +134 -0
  13. data/lib/active_support/cache/file_store.rb +49 -17
  14. data/lib/active_support/cache/mem_cache_store.rb +111 -129
  15. data/lib/active_support/cache/memory_store.rb +81 -26
  16. data/lib/active_support/cache/null_store.rb +6 -0
  17. data/lib/active_support/cache/redis_cache_store.rb +175 -154
  18. data/lib/active_support/cache/serializer_with_fallback.rb +152 -0
  19. data/lib/active_support/cache/strategy/local_cache.rb +31 -13
  20. data/lib/active_support/cache.rb +457 -377
  21. data/lib/active_support/callbacks.rb +123 -139
  22. data/lib/active_support/code_generator.rb +15 -10
  23. data/lib/active_support/concern.rb +4 -2
  24. data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +42 -3
  25. data/lib/active_support/concurrency/null_lock.rb +13 -0
  26. data/lib/active_support/configurable.rb +12 -2
  27. data/lib/active_support/core_ext/array/conversions.rb +7 -9
  28. data/lib/active_support/core_ext/array/inquiry.rb +2 -2
  29. data/lib/active_support/core_ext/array.rb +0 -1
  30. data/lib/active_support/core_ext/class/subclasses.rb +4 -15
  31. data/lib/active_support/core_ext/date/blank.rb +4 -0
  32. data/lib/active_support/core_ext/date/calculations.rb +20 -5
  33. data/lib/active_support/core_ext/date/conversions.rb +15 -16
  34. data/lib/active_support/core_ext/date.rb +0 -1
  35. data/lib/active_support/core_ext/date_and_time/calculations.rb +14 -4
  36. data/lib/active_support/core_ext/date_and_time/compatibility.rb +29 -2
  37. data/lib/active_support/core_ext/date_time/blank.rb +4 -0
  38. data/lib/active_support/core_ext/date_time/calculations.rb +4 -0
  39. data/lib/active_support/core_ext/date_time/conversions.rb +15 -15
  40. data/lib/active_support/core_ext/date_time.rb +0 -1
  41. data/lib/active_support/core_ext/digest/uuid.rb +7 -10
  42. data/lib/active_support/core_ext/enumerable.rb +51 -101
  43. data/lib/active_support/core_ext/erb/util.rb +201 -0
  44. data/lib/active_support/core_ext/file/atomic.rb +2 -0
  45. data/lib/active_support/core_ext/hash/conversions.rb +1 -2
  46. data/lib/active_support/core_ext/hash/deep_merge.rb +22 -14
  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 +7 -7
  50. data/lib/active_support/core_ext/integer/inflections.rb +12 -12
  51. data/lib/active_support/core_ext/kernel/singleton_class.rb +1 -1
  52. data/lib/active_support/core_ext/module/attr_internal.rb +17 -6
  53. data/lib/active_support/core_ext/module/attribute_accessors.rb +6 -0
  54. data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +38 -20
  55. data/lib/active_support/core_ext/module/concerning.rb +6 -6
  56. data/lib/active_support/core_ext/module/delegation.rb +20 -119
  57. data/lib/active_support/core_ext/module/deprecation.rb +12 -12
  58. data/lib/active_support/core_ext/module/introspection.rb +0 -1
  59. data/lib/active_support/core_ext/numeric/bytes.rb +9 -0
  60. data/lib/active_support/core_ext/numeric/conversions.rb +77 -75
  61. data/lib/active_support/core_ext/numeric.rb +0 -1
  62. data/lib/active_support/core_ext/object/acts_like.rb +29 -5
  63. data/lib/active_support/core_ext/object/blank.rb +45 -1
  64. data/lib/active_support/core_ext/object/deep_dup.rb +16 -0
  65. data/lib/active_support/core_ext/object/duplicable.rb +25 -16
  66. data/lib/active_support/core_ext/object/inclusion.rb +13 -5
  67. data/lib/active_support/core_ext/object/instance_variables.rb +4 -2
  68. data/lib/active_support/core_ext/object/json.rb +17 -7
  69. data/lib/active_support/core_ext/object/to_query.rb +0 -2
  70. data/lib/active_support/core_ext/object/with.rb +46 -0
  71. data/lib/active_support/core_ext/object/with_options.rb +9 -9
  72. data/lib/active_support/core_ext/object.rb +1 -0
  73. data/lib/active_support/core_ext/pathname/blank.rb +20 -0
  74. data/lib/active_support/core_ext/pathname/existence.rb +2 -0
  75. data/lib/active_support/core_ext/pathname.rb +1 -0
  76. data/lib/active_support/core_ext/range/conversions.rb +32 -11
  77. data/lib/active_support/core_ext/range/overlap.rb +40 -0
  78. data/lib/active_support/core_ext/range.rb +1 -2
  79. data/lib/active_support/core_ext/securerandom.rb +2 -6
  80. data/lib/active_support/core_ext/string/conversions.rb +3 -3
  81. data/lib/active_support/core_ext/string/filters.rb +21 -15
  82. data/lib/active_support/core_ext/string/indent.rb +1 -1
  83. data/lib/active_support/core_ext/string/inflections.rb +16 -9
  84. data/lib/active_support/core_ext/string/inquiry.rb +1 -1
  85. data/lib/active_support/core_ext/string/multibyte.rb +1 -1
  86. data/lib/active_support/core_ext/string/output_safety.rb +39 -150
  87. data/lib/active_support/core_ext/thread/backtrace/location.rb +12 -0
  88. data/lib/active_support/core_ext/time/calculations.rb +42 -32
  89. data/lib/active_support/core_ext/time/compatibility.rb +16 -0
  90. data/lib/active_support/core_ext/time/conversions.rb +13 -15
  91. data/lib/active_support/core_ext/time/zones.rb +8 -9
  92. data/lib/active_support/core_ext/time.rb +0 -1
  93. data/lib/active_support/core_ext.rb +0 -1
  94. data/lib/active_support/current_attributes.rb +53 -46
  95. data/lib/active_support/deep_mergeable.rb +53 -0
  96. data/lib/active_support/delegation.rb +202 -0
  97. data/lib/active_support/dependencies/autoload.rb +9 -16
  98. data/lib/active_support/deprecation/behaviors.rb +65 -42
  99. data/lib/active_support/deprecation/constant_accessor.rb +47 -25
  100. data/lib/active_support/deprecation/deprecators.rb +104 -0
  101. data/lib/active_support/deprecation/disallowed.rb +6 -8
  102. data/lib/active_support/deprecation/method_wrappers.rb +6 -23
  103. data/lib/active_support/deprecation/proxy_wrappers.rb +34 -22
  104. data/lib/active_support/deprecation/reporting.rb +49 -27
  105. data/lib/active_support/deprecation.rb +39 -9
  106. data/lib/active_support/deprecator.rb +7 -0
  107. data/lib/active_support/descendants_tracker.rb +66 -175
  108. data/lib/active_support/duration/iso8601_parser.rb +2 -2
  109. data/lib/active_support/duration/iso8601_serializer.rb +1 -4
  110. data/lib/active_support/duration.rb +13 -7
  111. data/lib/active_support/encrypted_configuration.rb +63 -10
  112. data/lib/active_support/encrypted_file.rb +29 -13
  113. data/lib/active_support/environment_inquirer.rb +22 -2
  114. data/lib/active_support/error_reporter/test_helper.rb +15 -0
  115. data/lib/active_support/error_reporter.rb +160 -36
  116. data/lib/active_support/evented_file_update_checker.rb +19 -7
  117. data/lib/active_support/execution_wrapper.rb +23 -28
  118. data/lib/active_support/file_update_checker.rb +5 -3
  119. data/lib/active_support/fork_tracker.rb +4 -32
  120. data/lib/active_support/gem_version.rb +4 -4
  121. data/lib/active_support/gzip.rb +2 -0
  122. data/lib/active_support/hash_with_indifferent_access.rb +41 -25
  123. data/lib/active_support/html_safe_translation.rb +19 -6
  124. data/lib/active_support/i18n.rb +1 -1
  125. data/lib/active_support/i18n_railtie.rb +20 -13
  126. data/lib/active_support/inflector/inflections.rb +2 -0
  127. data/lib/active_support/inflector/methods.rb +28 -18
  128. data/lib/active_support/inflector/transliterate.rb +4 -2
  129. data/lib/active_support/isolated_execution_state.rb +39 -19
  130. data/lib/active_support/json/decoding.rb +2 -1
  131. data/lib/active_support/json/encoding.rb +25 -43
  132. data/lib/active_support/key_generator.rb +13 -5
  133. data/lib/active_support/lazy_load_hooks.rb +33 -7
  134. data/lib/active_support/locale/en.yml +2 -0
  135. data/lib/active_support/log_subscriber/test_helper.rb +2 -2
  136. data/lib/active_support/log_subscriber.rb +76 -36
  137. data/lib/active_support/logger.rb +22 -60
  138. data/lib/active_support/logger_thread_safe_level.rb +10 -32
  139. data/lib/active_support/message_encryptor.rb +200 -55
  140. data/lib/active_support/message_encryptors.rb +141 -0
  141. data/lib/active_support/message_pack/cache_serializer.rb +23 -0
  142. data/lib/active_support/message_pack/extensions.rb +305 -0
  143. data/lib/active_support/message_pack/serializer.rb +63 -0
  144. data/lib/active_support/message_pack.rb +50 -0
  145. data/lib/active_support/message_verifier.rb +220 -89
  146. data/lib/active_support/message_verifiers.rb +135 -0
  147. data/lib/active_support/messages/codec.rb +65 -0
  148. data/lib/active_support/messages/metadata.rb +111 -45
  149. data/lib/active_support/messages/rotation_coordinator.rb +93 -0
  150. data/lib/active_support/messages/rotator.rb +34 -32
  151. data/lib/active_support/messages/serializer_with_fallback.rb +158 -0
  152. data/lib/active_support/multibyte/chars.rb +4 -2
  153. data/lib/active_support/multibyte/unicode.rb +9 -37
  154. data/lib/active_support/notifications/fanout.rb +248 -87
  155. data/lib/active_support/notifications/instrumenter.rb +93 -25
  156. data/lib/active_support/notifications.rb +38 -31
  157. data/lib/active_support/number_helper/number_converter.rb +16 -7
  158. data/lib/active_support/number_helper/number_to_currency_converter.rb +6 -6
  159. data/lib/active_support/number_helper/number_to_human_size_converter.rb +3 -3
  160. data/lib/active_support/number_helper/number_to_phone_converter.rb +1 -0
  161. data/lib/active_support/number_helper.rb +379 -317
  162. data/lib/active_support/option_merger.rb +4 -4
  163. data/lib/active_support/ordered_hash.rb +3 -3
  164. data/lib/active_support/ordered_options.rb +68 -16
  165. data/lib/active_support/parameter_filter.rb +103 -84
  166. data/lib/active_support/proxy_object.rb +8 -3
  167. data/lib/active_support/railtie.rb +30 -25
  168. data/lib/active_support/reloader.rb +13 -5
  169. data/lib/active_support/rescuable.rb +12 -10
  170. data/lib/active_support/secure_compare_rotator.rb +17 -10
  171. data/lib/active_support/string_inquirer.rb +4 -2
  172. data/lib/active_support/subscriber.rb +10 -27
  173. data/lib/active_support/syntax_error_proxy.rb +60 -0
  174. data/lib/active_support/tagged_logging.rb +64 -25
  175. data/lib/active_support/test_case.rb +160 -7
  176. data/lib/active_support/testing/assertions.rb +29 -13
  177. data/lib/active_support/testing/autorun.rb +0 -2
  178. data/lib/active_support/testing/constant_stubbing.rb +54 -0
  179. data/lib/active_support/testing/deprecation.rb +20 -27
  180. data/lib/active_support/testing/error_reporter_assertions.rb +107 -0
  181. data/lib/active_support/testing/isolation.rb +46 -33
  182. data/lib/active_support/testing/method_call_assertions.rb +7 -8
  183. data/lib/active_support/testing/parallelization/server.rb +3 -0
  184. data/lib/active_support/testing/parallelize_executor.rb +8 -3
  185. data/lib/active_support/testing/setup_and_teardown.rb +2 -0
  186. data/lib/active_support/testing/stream.rb +1 -1
  187. data/lib/active_support/testing/strict_warnings.rb +43 -0
  188. data/lib/active_support/testing/tests_without_assertions.rb +19 -0
  189. data/lib/active_support/testing/time_helpers.rb +38 -16
  190. data/lib/active_support/time_with_zone.rb +28 -54
  191. data/lib/active_support/values/time_zone.rb +26 -15
  192. data/lib/active_support/version.rb +1 -1
  193. data/lib/active_support/xml_mini/jdom.rb +3 -10
  194. data/lib/active_support/xml_mini/nokogiri.rb +1 -1
  195. data/lib/active_support/xml_mini/nokogirisax.rb +1 -1
  196. data/lib/active_support/xml_mini/rexml.rb +1 -1
  197. data/lib/active_support/xml_mini.rb +13 -4
  198. data/lib/active_support.rb +15 -3
  199. metadata +142 -21
  200. data/lib/active_support/core_ext/array/deprecated_conversions.rb +0 -25
  201. data/lib/active_support/core_ext/date/deprecated_conversions.rb +0 -26
  202. data/lib/active_support/core_ext/date_time/deprecated_conversions.rb +0 -22
  203. data/lib/active_support/core_ext/numeric/deprecated_conversions.rb +0 -60
  204. data/lib/active_support/core_ext/range/deprecated_conversions.rb +0 -26
  205. data/lib/active_support/core_ext/range/include_time_with_zone.rb +0 -7
  206. data/lib/active_support/core_ext/range/overlaps.rb +0 -10
  207. data/lib/active_support/core_ext/time/deprecated_conversions.rb +0 -22
  208. data/lib/active_support/core_ext/uri.rb +0 -5
  209. data/lib/active_support/deprecation/instance_delegator.rb +0 -38
  210. data/lib/active_support/per_thread_registry.rb +0 -65
  211. data/lib/active_support/ruby_features.rb +0 -7
@@ -2,14 +2,15 @@
2
2
 
3
3
  require "zlib"
4
4
  require "active_support/core_ext/array/extract_options"
5
- require "active_support/core_ext/array/wrap"
6
5
  require "active_support/core_ext/enumerable"
7
6
  require "active_support/core_ext/module/attribute_accessors"
8
7
  require "active_support/core_ext/numeric/bytes"
9
- require "active_support/core_ext/numeric/time"
10
8
  require "active_support/core_ext/object/to_param"
11
9
  require "active_support/core_ext/object/try"
12
10
  require "active_support/core_ext/string/inflections"
11
+ require_relative "cache/coder"
12
+ require_relative "cache/entry"
13
+ require_relative "cache/serializer_with_fallback"
13
14
 
14
15
  module ActiveSupport
15
16
  # See ActiveSupport::Cache::Store for documentation.
@@ -22,20 +23,36 @@ module ActiveSupport
22
23
 
23
24
  # These options mean something to all cache implementations. Individual cache
24
25
  # implementations may support additional options.
25
- UNIVERSAL_OPTIONS = [:namespace, :compress, :compress_threshold, :expires_in, :expire_in, :expired_in, :race_condition_ttl, :coder, :skip_nil]
26
-
27
- DEFAULT_COMPRESS_LIMIT = 1.kilobyte
26
+ UNIVERSAL_OPTIONS = [
27
+ :coder,
28
+ :compress,
29
+ :compress_threshold,
30
+ :compressor,
31
+ :expire_in,
32
+ :expired_in,
33
+ :expires_in,
34
+ :namespace,
35
+ :race_condition_ttl,
36
+ :serializer,
37
+ :skip_nil,
38
+ ]
28
39
 
29
40
  # Mapping of canonical option names to aliases that a store will recognize.
30
41
  OPTION_ALIASES = {
31
42
  expires_in: [:expire_in, :expired_in]
32
43
  }.freeze
33
44
 
45
+ DEFAULT_COMPRESS_LIMIT = 1.kilobyte
46
+
47
+ # Raised by coders when the cache entry can't be deserialized.
48
+ # This error is treated as a cache miss.
49
+ DeserializationError = Class.new(StandardError)
50
+
34
51
  module Strategy
35
52
  autoload :LocalCache, "active_support/cache/strategy/local_cache"
36
53
  end
37
54
 
38
- @format_version = 6.1
55
+ @format_version = 7.0
39
56
 
40
57
  class << self
41
58
  attr_accessor :format_version
@@ -69,13 +86,7 @@ module ActiveSupport
69
86
  case store
70
87
  when Symbol
71
88
  options = parameters.extract_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
89
+ retrieve_store_class(store).new(*parameters, **options)
79
90
  when Array
80
91
  lookup_store(*store)
81
92
  when nil
@@ -132,6 +143,8 @@ module ActiveSupport
132
143
  end
133
144
  end
134
145
 
146
+ # = Active Support \Cache \Store
147
+ #
135
148
  # An abstract cache store class. There are multiple cache store
136
149
  # implementations, each having its own additional features. See the classes
137
150
  # under the ActiveSupport::Cache module, e.g.
@@ -139,16 +152,19 @@ module ActiveSupport
139
152
  # popular cache store for large production websites.
140
153
  #
141
154
  # Some implementations may not support all methods beyond the basic cache
142
- # methods of +fetch+, +write+, +read+, +exist?+, and +delete+.
155
+ # methods of #fetch, #write, #read, #exist?, and #delete.
143
156
  #
144
- # ActiveSupport::Cache::Store can store any serializable Ruby object.
157
+ # +ActiveSupport::Cache::Store+ can store any Ruby object that is supported
158
+ # by its +coder+'s +dump+ and +load+ methods.
145
159
  #
146
160
  # cache = ActiveSupport::Cache::MemoryStore.new
147
161
  #
148
162
  # cache.read('city') # => nil
149
- # cache.write('city', "Duckburgh")
163
+ # cache.write('city', "Duckburgh") # => true
150
164
  # cache.read('city') # => "Duckburgh"
151
165
  #
166
+ # cache.write('not serializable', Proc.new {}) # => TypeError
167
+ #
152
168
  # Keys are always translated into Strings and are case sensitive. When an
153
169
  # object is specified as a key and has a +cache_key+ method defined, this
154
170
  # method will be called to define the key. Otherwise, the +to_param+
@@ -169,43 +185,130 @@ module ActiveSupport
169
185
  # cache.namespace = -> { @last_mod_time } # Set the namespace to a variable
170
186
  # @last_mod_time = Time.now # Invalidate the entire cache by changing namespace
171
187
  #
172
- # Cached data larger than 1kB are compressed by default. To turn off
173
- # compression, pass <tt>compress: false</tt> to the initializer or to
174
- # individual +fetch+ or +write+ method calls. The 1kB compression
175
- # threshold is configurable with the <tt>:compress_threshold</tt> option,
176
- # specified in bytes.
177
188
  class Store
178
189
  cattr_accessor :logger, instance_writer: true
190
+ cattr_accessor :raise_on_invalid_cache_expiration_time, default: false
179
191
 
180
192
  attr_reader :silence, :options
181
193
  alias :silence? :silence
182
194
 
183
195
  class << self
184
196
  private
197
+ DEFAULT_POOL_OPTIONS = { size: 5, timeout: 5 }.freeze
198
+ private_constant :DEFAULT_POOL_OPTIONS
199
+
185
200
  def retrieve_pool_options(options)
186
- {}.tap do |pool_options|
187
- pool_options[:size] = options.delete(:pool_size) if options[:pool_size]
188
- pool_options[:timeout] = options.delete(:pool_timeout) if options[:pool_timeout]
201
+ if options.key?(:pool)
202
+ pool_options = options.delete(:pool)
203
+ else
204
+ pool_options = true
189
205
  end
190
- end
191
206
 
192
- def ensure_connection_pool_added!
193
- require "connection_pool"
194
- rescue LoadError => e
195
- $stderr.puts "You don't have connection_pool installed in your application. Please add it to your Gemfile and run bundle install"
196
- raise e
207
+ case pool_options
208
+ when false, nil
209
+ return false
210
+ when true
211
+ pool_options = DEFAULT_POOL_OPTIONS
212
+ when Hash
213
+ pool_options[:size] = Integer(pool_options[:size]) if pool_options.key?(:size)
214
+ pool_options[:timeout] = Float(pool_options[:timeout]) if pool_options.key?(:timeout)
215
+ pool_options = DEFAULT_POOL_OPTIONS.merge(pool_options)
216
+ else
217
+ raise TypeError, "Invalid :pool argument, expected Hash, got: #{pool_options.inspect}"
218
+ end
219
+
220
+ pool_options unless pool_options.empty?
197
221
  end
198
222
  end
199
223
 
200
- # Creates a new cache. The options will be passed to any write method calls
201
- # except for <tt>:namespace</tt> which can be used to set the global
202
- # namespace for the cache.
224
+ # Creates a new cache.
225
+ #
226
+ # ==== Options
227
+ #
228
+ # [+:namespace+]
229
+ # Sets the namespace for the cache. This option is especially useful if
230
+ # your application shares a cache with other applications.
231
+ #
232
+ # [+:serializer+]
233
+ # The serializer for cached values. Must respond to +dump+ and +load+.
234
+ #
235
+ # The default serializer depends on the cache format version (set via
236
+ # +config.active_support.cache_format_version+ when using Rails). The
237
+ # default serializer for each format version includes a fallback
238
+ # mechanism to deserialize values from any format version. This behavior
239
+ # makes it easy to migrate between format versions without invalidating
240
+ # the entire cache.
241
+ #
242
+ # You can also specify <tt>serializer: :message_pack</tt> to use a
243
+ # preconfigured serializer based on ActiveSupport::MessagePack. The
244
+ # +:message_pack+ serializer includes the same deserialization fallback
245
+ # mechanism, allowing easy migration from (or to) the default
246
+ # serializer. The +:message_pack+ serializer may improve performance,
247
+ # but it requires the +msgpack+ gem.
248
+ #
249
+ # [+:compressor+]
250
+ # The compressor for serialized cache values. Must respond to +deflate+
251
+ # and +inflate+.
252
+ #
253
+ # The default compressor is +Zlib+. To define a new custom compressor
254
+ # that also decompresses old cache entries, you can check compressed
255
+ # values for Zlib's <tt>"\x78"</tt> signature:
256
+ #
257
+ # module MyCompressor
258
+ # def self.deflate(dumped)
259
+ # # compression logic... (make sure result does not start with "\x78"!)
260
+ # end
261
+ #
262
+ # def self.inflate(compressed)
263
+ # if compressed.start_with?("\x78")
264
+ # Zlib.inflate(compressed)
265
+ # else
266
+ # # decompression logic...
267
+ # end
268
+ # end
269
+ # end
270
+ #
271
+ # ActiveSupport::Cache.lookup_store(:redis_cache_store, compressor: MyCompressor)
272
+ #
273
+ # [+:coder+]
274
+ # The coder for serializing and (optionally) compressing cache entries.
275
+ # Must respond to +dump+ and +load+.
276
+ #
277
+ # The default coder composes the serializer and compressor, and includes
278
+ # some performance optimizations. If you only need to override the
279
+ # serializer or compressor, you should specify the +:serializer+ or
280
+ # +:compressor+ options instead.
281
+ #
282
+ # If the store can handle cache entries directly, you may also specify
283
+ # <tt>coder: nil</tt> to omit the serializer, compressor, and coder. For
284
+ # example, if you are using ActiveSupport::Cache::MemoryStore and can
285
+ # guarantee that cache values will not be mutated, you can specify
286
+ # <tt>coder: nil</tt> to avoid the overhead of safeguarding against
287
+ # mutation.
288
+ #
289
+ # The +:coder+ option is mutally exclusive with the +:serializer+ and
290
+ # +:compressor+ options. Specifying them together will raise an
291
+ # +ArgumentError+.
292
+ #
293
+ # Any other specified options are treated as default options for the
294
+ # relevant cache operations, such as #read, #write, and #fetch.
203
295
  def initialize(options = nil)
204
- @options = options ? normalize_options(options) : {}
296
+ @options = options ? validate_options(normalize_options(options)) : {}
297
+
205
298
  @options[:compress] = true unless @options.key?(:compress)
206
- @options[:compress_threshold] = DEFAULT_COMPRESS_LIMIT unless @options.key?(:compress_threshold)
299
+ @options[:compress_threshold] ||= DEFAULT_COMPRESS_LIMIT
300
+
301
+ @coder = @options.delete(:coder) do
302
+ legacy_serializer = Cache.format_version < 7.1 && !@options[:serializer]
303
+ serializer = @options.delete(:serializer) || default_serializer
304
+ serializer = Cache::SerializerWithFallback[serializer] if serializer.is_a?(Symbol)
305
+ compressor = @options.delete(:compressor) { Zlib }
306
+
307
+ Cache::Coder.new(serializer, compressor, legacy_serializer: legacy_serializer)
308
+ end
309
+
310
+ @coder ||= Cache::SerializerWithFallback[:passthrough]
207
311
 
208
- @coder = @options.delete(:coder) { default_coder } || NullCoder
209
312
  @coder_supports_compression = @coder.respond_to?(:dump_compressed)
210
313
  end
211
314
 
@@ -217,7 +320,7 @@ module ActiveSupport
217
320
 
218
321
  # Silences the logger within a block.
219
322
  def mute
220
- previous_silence, @silence = defined?(@silence) && @silence, true
323
+ previous_silence, @silence = @silence, true
221
324
  yield
222
325
  ensure
223
326
  @silence = previous_silence
@@ -241,129 +344,133 @@ module ActiveSupport
241
344
  # end
242
345
  # cache.fetch('city') # => "Duckburgh"
243
346
  #
244
- # You may also specify additional options via the +options+ argument.
245
- # Setting <tt>force: true</tt> forces a cache "miss," meaning we treat
246
- # the cache value as missing even if it's present. Passing a block is
247
- # required when +force+ is true so this always results in a cache write.
347
+ # ==== Options
248
348
  #
249
- # cache.write('today', 'Monday')
250
- # cache.fetch('today', force: true) { 'Tuesday' } # => 'Tuesday'
251
- # cache.fetch('today', force: true) # => ArgumentError
252
- #
253
- # The +:force+ option is useful when you're calling some other method to
254
- # ask whether you should force a cache write. Otherwise, it's clearer to
255
- # just call <tt>Cache#write</tt>.
256
- #
257
- # Setting <tt>skip_nil: true</tt> will not cache nil result:
258
- #
259
- # cache.fetch('foo') { nil }
260
- # cache.fetch('bar', skip_nil: true) { nil }
261
- # cache.exist?('foo') # => true
262
- # cache.exist?('bar') # => false
263
- #
264
- #
265
- # Setting <tt>compress: false</tt> disables compression of the cache entry.
266
- #
267
- # Setting <tt>:expires_in</tt> will set an expiration time on the cache.
268
- # All caches support auto-expiring content after a specified number of
269
- # seconds. This value can be specified as an option to the constructor
270
- # (in which case all entries will be affected), or it can be supplied to
271
- # the +fetch+ or +write+ method to affect just one entry.
272
- # <tt>:expire_in</tt> and <tt>:expired_in</tt> are aliases for
273
- # <tt>:expires_in</tt>.
274
- #
275
- # cache = ActiveSupport::Cache::MemoryStore.new(expires_in: 5.minutes)
276
- # cache.write(key, value, expires_in: 1.minute) # Set a lower value for one entry
277
- #
278
- # Setting <tt>:expires_at</tt> will set an absolute expiration time on the cache.
279
- # All caches support auto-expiring content after a specified number of
280
- # seconds. This value can only be supplied to the +fetch+ or +write+ method to
281
- # affect just one entry.
282
- #
283
- # cache = ActiveSupport::Cache::MemoryStore.new
284
- # cache.write(key, value, expires_at: Time.now.at_end_of_hour)
285
- #
286
- # Setting <tt>:version</tt> verifies the cache stored under <tt>name</tt>
287
- # is of the same version. nil is returned on mismatches despite contents.
288
- # This feature is used to support recyclable cache keys.
289
- #
290
- # Setting <tt>:race_condition_ttl</tt> is very useful in situations where
291
- # a cache entry is used very frequently and is under heavy load. If a
292
- # cache expires and due to heavy load several different processes will try
293
- # to read data natively and then they all will try to write to cache. To
294
- # avoid that case the first process to find an expired cache entry will
295
- # bump the cache expiration time by the value set in <tt>:race_condition_ttl</tt>.
296
- # Yes, this process is extending the time for a stale value by another few
297
- # seconds. Because of extended life of the previous cache, other processes
298
- # will continue to use slightly stale data for a just a bit longer. In the
299
- # meantime that first process will go ahead and will write into cache the
300
- # new value. After that all the processes will start getting the new value.
301
- # The key is to keep <tt>:race_condition_ttl</tt> small.
302
- #
303
- # If the process regenerating the entry errors out, the entry will be
304
- # regenerated after the specified number of seconds. Also note that the
305
- # life of stale cache is extended only if it expired recently. Otherwise
306
- # a new value is generated and <tt>:race_condition_ttl</tt> does not play
307
- # any role.
308
- #
309
- # # Set all values to expire after one minute.
310
- # cache = ActiveSupport::Cache::MemoryStore.new(expires_in: 1.minute)
311
- #
312
- # cache.write('foo', 'original value')
313
- # val_1 = nil
314
- # val_2 = nil
315
- # sleep 60
316
- #
317
- # Thread.new do
318
- # val_1 = cache.fetch('foo', race_condition_ttl: 10.seconds) do
319
- # sleep 1
320
- # 'new value 1'
349
+ # Internally, +fetch+ calls +read_entry+, and calls +write_entry+ on a
350
+ # cache miss. Thus, +fetch+ supports the same options as #read and #write.
351
+ # Additionally, +fetch+ supports the following options:
352
+ #
353
+ # * <tt>force: true</tt> - Forces a cache "miss," meaning we treat the
354
+ # cache value as missing even if it's present. Passing a block is
355
+ # required when +force+ is true so this always results in a cache write.
356
+ #
357
+ # cache.write('today', 'Monday')
358
+ # cache.fetch('today', force: true) { 'Tuesday' } # => 'Tuesday'
359
+ # cache.fetch('today', force: true) # => ArgumentError
360
+ #
361
+ # The +:force+ option is useful when you're calling some other method to
362
+ # ask whether you should force a cache write. Otherwise, it's clearer to
363
+ # just call +write+.
364
+ #
365
+ # * <tt>skip_nil: true</tt> - Prevents caching a nil result:
366
+ #
367
+ # cache.fetch('foo') { nil }
368
+ # cache.fetch('bar', skip_nil: true) { nil }
369
+ # cache.exist?('foo') # => true
370
+ # cache.exist?('bar') # => false
371
+ #
372
+ # * +:race_condition_ttl+ - Specifies the number of seconds during which
373
+ # an expired value can be reused while a new value is being generated.
374
+ # This can be used to prevent race conditions when cache entries expire,
375
+ # by preventing multiple processes from simultaneously regenerating the
376
+ # same entry (also known as the dog pile effect).
377
+ #
378
+ # When a process encounters a cache entry that has expired less than
379
+ # +:race_condition_ttl+ seconds ago, it will bump the expiration time by
380
+ # +:race_condition_ttl+ seconds before generating a new value. During
381
+ # this extended time window, while the process generates a new value,
382
+ # other processes will continue to use the old value. After the first
383
+ # process writes the new value, other processes will then use it.
384
+ #
385
+ # If the first process errors out while generating a new value, another
386
+ # process can try to generate a new value after the extended time window
387
+ # has elapsed.
388
+ #
389
+ # # Set all values to expire after one minute.
390
+ # cache = ActiveSupport::Cache::MemoryStore.new(expires_in: 1)
391
+ #
392
+ # cache.write("foo", "original value")
393
+ # val_1 = nil
394
+ # val_2 = nil
395
+ # p cache.read("foo") # => "original value"
396
+ #
397
+ # sleep 1 # wait until the cache expires
398
+ #
399
+ # t1 = Thread.new do
400
+ # # fetch does the following:
401
+ # # 1. gets an recent expired entry
402
+ # # 2. extends the expiry by 2 seconds (race_condition_ttl)
403
+ # # 3. regenerates the new value
404
+ # val_1 = cache.fetch("foo", race_condition_ttl: 2) do
405
+ # sleep 1
406
+ # "new value 1"
407
+ # end
321
408
  # end
322
- # end
323
409
  #
324
- # Thread.new do
325
- # val_2 = cache.fetch('foo', race_condition_ttl: 10.seconds) do
326
- # 'new value 2'
410
+ # # Wait until t1 extends the expiry of the entry
411
+ # # but before generating the new value
412
+ # sleep 0.1
413
+ #
414
+ # val_2 = cache.fetch("foo", race_condition_ttl: 2) do
415
+ # # This block won't be executed because t1 extended the expiry
416
+ # "new value 2"
327
417
  # end
328
- # end
329
418
  #
330
- # cache.fetch('foo') # => "original value"
331
- # sleep 10 # First thread extended the life of cache by another 10 seconds
332
- # cache.fetch('foo') # => "new value 1"
333
- # val_1 # => "new value 1"
334
- # val_2 # => "original value"
419
+ # t1.join
335
420
  #
336
- # Other options will be handled by the specific cache store implementation.
337
- # Internally, #fetch calls #read_entry, and calls #write_entry on a cache
338
- # miss. +options+ will be passed to the #read and #write calls.
421
+ # p val_1 # => "new value 1"
422
+ # p val_2 # => "oritinal value"
423
+ # p cache.fetch("foo") # => "new value 1"
339
424
  #
340
- # For example, MemCacheStore's #write method supports the +:raw+
341
- # option, which tells the memcached server to store all values as strings.
342
- # We can use this option with #fetch too:
425
+ # # The entry requires 3 seconds to expire (expires_in + race_condition_ttl)
426
+ # # We have waited 2 seconds already (sleep(1) + t1.join) thus we need to wait 1
427
+ # # more second to see the entry expire.
428
+ # sleep 1
429
+ #
430
+ # p cache.fetch("foo") # => nil
431
+ #
432
+ # ==== Dynamic Options
433
+ #
434
+ # In some cases it may be necessary to dynamically compute options based
435
+ # on the cached value. To support this, an ActiveSupport::Cache::WriteOptions
436
+ # instance is passed as the second argument to the block. For example:
437
+ #
438
+ # cache.fetch("authentication-token:#{user.id}") do |key, options|
439
+ # token = authenticate_to_service
440
+ # options.expires_at = token.expires_at
441
+ # token
442
+ # end
343
443
  #
344
- # cache = ActiveSupport::Cache::MemCacheStore.new
345
- # cache.fetch("foo", force: true, raw: true) do
346
- # :bar
347
- # end
348
- # cache.fetch('foo') # => "bar"
349
444
  def fetch(name, options = nil, &block)
350
445
  if block_given?
351
446
  options = merged_options(options)
352
447
  key = normalize_key(name, options)
353
448
 
354
449
  entry = nil
355
- instrument(:read, name, options) do |payload|
356
- cached_entry = read_entry(key, **options, event: payload) unless options[:force]
357
- entry = handle_expired_entry(cached_entry, key, options)
358
- entry = nil if entry && entry.mismatched?(normalize_version(name, options))
359
- payload[:super_operation] = :fetch if payload
360
- payload[:hit] = !!entry if payload
450
+ unless options[:force]
451
+ instrument(:read, key, options) do |payload|
452
+ cached_entry = read_entry(key, **options, event: payload)
453
+ entry = handle_expired_entry(cached_entry, key, options)
454
+ if entry
455
+ if entry.mismatched?(normalize_version(name, options))
456
+ entry = nil
457
+ else
458
+ begin
459
+ entry.value
460
+ rescue DeserializationError
461
+ entry = nil
462
+ end
463
+ end
464
+ end
465
+ payload[:super_operation] = :fetch if payload
466
+ payload[:hit] = !!entry if payload
467
+ end
361
468
  end
362
469
 
363
470
  if entry
364
471
  get_entry_value(entry, name, options)
365
472
  else
366
- save_block_result_to_cache(name, options, &block)
473
+ save_block_result_to_cache(name, key, options, &block)
367
474
  end
368
475
  elsif options && options[:force]
369
476
  raise ArgumentError, "Missing block: Calling `Cache#fetch` with `force: true` requires a block."
@@ -380,13 +487,20 @@ module ActiveSupport
380
487
  # <tt>:version</tt> options, both of these conditions are applied before
381
488
  # the data is returned.
382
489
  #
383
- # Options are passed to the underlying cache implementation.
490
+ # ==== Options
491
+ #
492
+ # * +:namespace+ - Replace the store namespace for this call.
493
+ # * +:version+ - Specifies a version for the cache entry. If the cached
494
+ # version does not match the requested version, the read will be treated
495
+ # as a cache miss. This feature is used to support recyclable cache keys.
496
+ #
497
+ # Other options will be handled by the specific cache store implementation.
384
498
  def read(name, options = nil)
385
499
  options = merged_options(options)
386
500
  key = normalize_key(name, options)
387
501
  version = normalize_version(name, options)
388
502
 
389
- instrument(:read, name, options) do |payload|
503
+ instrument(:read, key, options) do |payload|
390
504
  entry = read_entry(key, **options, event: payload)
391
505
 
392
506
  if entry
@@ -399,7 +513,12 @@ module ActiveSupport
399
513
  nil
400
514
  else
401
515
  payload[:hit] = true if payload
402
- entry.value
516
+ begin
517
+ entry.value
518
+ rescue DeserializationError
519
+ payload[:hit] = false
520
+ nil
521
+ end
403
522
  end
404
523
  else
405
524
  payload[:hit] = false if payload
@@ -415,10 +534,12 @@ module ActiveSupport
415
534
  #
416
535
  # Returns a hash mapping the names provided to the values found.
417
536
  def read_multi(*names)
537
+ return {} if names.empty?
538
+
418
539
  options = names.extract_options!
419
540
  options = merged_options(options)
420
541
 
421
- instrument :read_multi, names, options do |payload|
542
+ instrument_multi :read_multi, names, options do |payload|
422
543
  read_multi_entries(names, **options, event: payload).tap do |results|
423
544
  payload[:hits] = results.keys
424
545
  end
@@ -427,9 +548,11 @@ module ActiveSupport
427
548
 
428
549
  # Cache Storage API to write multiple values at once.
429
550
  def write_multi(hash, options = nil)
551
+ return hash if hash.empty?
552
+
430
553
  options = merged_options(options)
431
554
 
432
- instrument :write_multi, hash, options do |payload|
555
+ instrument_multi :write_multi, hash, options do |payload|
433
556
  entries = hash.each_with_object({}) do |(name, value), memo|
434
557
  memo[normalize_key(name, options)] = Entry.new(value, **options.merge(version: normalize_version(name, options)))
435
558
  end
@@ -455,7 +578,8 @@ module ActiveSupport
455
578
  # # => { "bim" => "bam",
456
579
  # # "unknown_key" => "Fallback value for key: unknown_key" }
457
580
  #
458
- # Options are passed to the underlying cache implementation. For example:
581
+ # You may also specify additional options via the +options+ argument. See #fetch for details.
582
+ # Other options are passed to the underlying cache implementation. For example:
459
583
  #
460
584
  # cache.fetch_multi("fizz", expires_in: 5.seconds) do |key|
461
585
  # "buzz"
@@ -468,57 +592,105 @@ module ActiveSupport
468
592
  # # => nil
469
593
  def fetch_multi(*names)
470
594
  raise ArgumentError, "Missing block: `Cache#fetch_multi` requires a block." unless block_given?
595
+ return {} if names.empty?
471
596
 
472
597
  options = names.extract_options!
473
598
  options = merged_options(options)
474
599
 
475
- instrument :read_multi, names, options do |payload|
476
- reads = read_multi_entries(names, **options)
477
- writes = {}
600
+ writes = {}
601
+ ordered = instrument_multi :read_multi, names, options do |payload|
602
+ if options[:force]
603
+ reads = {}
604
+ else
605
+ reads = read_multi_entries(names, **options)
606
+ end
607
+
478
608
  ordered = names.index_with do |name|
479
609
  reads.fetch(name) { writes[name] = yield(name) }
480
610
  end
611
+ writes.compact! if options[:skip_nil]
481
612
 
482
613
  payload[:hits] = reads.keys
483
614
  payload[:super_operation] = :fetch_multi
484
615
 
485
- write_multi(writes, options)
486
-
487
616
  ordered
488
617
  end
618
+
619
+ write_multi(writes, options)
620
+
621
+ ordered
489
622
  end
490
623
 
491
- # Writes the value to the cache, with the key.
624
+ # Writes the value to the cache with the key. The value must be supported
625
+ # by the +coder+'s +dump+ and +load+ methods.
492
626
  #
493
- # Options are passed to the underlying cache implementation.
627
+ # Returns +true+ if the write succeeded, +nil+ if there was an error talking
628
+ # to the cache backend, or +false+ if the write failed for another reason.
629
+ #
630
+ # By default, cache entries larger than 1kB are compressed. Compression
631
+ # allows more data to be stored in the same memory footprint, leading to
632
+ # fewer cache evictions and higher hit rates.
633
+ #
634
+ # ==== Options
635
+ #
636
+ # * <tt>compress: false</tt> - Disables compression of the cache entry.
637
+ #
638
+ # * +:compress_threshold+ - The compression threshold, specified in bytes.
639
+ # \Cache entries larger than this threshold will be compressed. Defaults
640
+ # to +1.kilobyte+.
641
+ #
642
+ # * +:expires_in+ - Sets a relative expiration time for the cache entry,
643
+ # specified in seconds. +:expire_in+ and +:expired_in+ are aliases for
644
+ # +:expires_in+.
645
+ #
646
+ # cache = ActiveSupport::Cache::MemoryStore.new(expires_in: 5.minutes)
647
+ # cache.write(key, value, expires_in: 1.minute) # Set a lower value for one entry
648
+ #
649
+ # * +:expires_at+ - Sets an absolute expiration time for the cache entry.
650
+ #
651
+ # cache = ActiveSupport::Cache::MemoryStore.new
652
+ # cache.write(key, value, expires_at: Time.now.at_end_of_hour)
653
+ #
654
+ # * +:version+ - Specifies a version for the cache entry. When reading
655
+ # from the cache, if the cached version does not match the requested
656
+ # version, the read will be treated as a cache miss. This feature is
657
+ # used to support recyclable cache keys.
658
+ #
659
+ # Other options will be handled by the specific cache store implementation.
494
660
  def write(name, value, options = nil)
495
661
  options = merged_options(options)
662
+ key = normalize_key(name, options)
496
663
 
497
- instrument(:write, name, options) do
664
+ instrument(:write, key, options) do
498
665
  entry = Entry.new(value, **options.merge(version: normalize_version(name, options)))
499
- write_entry(normalize_key(name, options), entry, **options)
666
+ write_entry(key, entry, **options)
500
667
  end
501
668
  end
502
669
 
503
- # Deletes an entry in the cache. Returns +true+ if an entry is deleted.
670
+ # Deletes an entry in the cache. Returns +true+ if an entry is deleted
671
+ # and +false+ otherwise.
504
672
  #
505
673
  # Options are passed to the underlying cache implementation.
506
674
  def delete(name, options = nil)
507
675
  options = merged_options(options)
676
+ key = normalize_key(name, options)
508
677
 
509
- instrument(:delete, name) do
510
- delete_entry(normalize_key(name, options), **options)
678
+ instrument(:delete, key, options) do
679
+ delete_entry(key, **options)
511
680
  end
512
681
  end
513
682
 
514
- # Deletes multiple entries in the cache.
683
+ # Deletes multiple entries in the cache. Returns the number of deleted
684
+ # entries.
515
685
  #
516
686
  # Options are passed to the underlying cache implementation.
517
687
  def delete_multi(names, options = nil)
688
+ return 0 if names.empty?
689
+
518
690
  options = merged_options(options)
519
691
  names.map! { |key| normalize_key(key, options) }
520
692
 
521
- instrument :delete_multi, names do
693
+ instrument_multi(:delete_multi, names, options) do
522
694
  delete_multi_entries(names, **options)
523
695
  end
524
696
  end
@@ -528,9 +700,10 @@ module ActiveSupport
528
700
  # Options are passed to the underlying cache implementation.
529
701
  def exist?(name, options = nil)
530
702
  options = merged_options(options)
703
+ key = normalize_key(name, options)
531
704
 
532
- instrument(:exist?, name) do |payload|
533
- entry = read_entry(normalize_key(name, options), **options, event: payload)
705
+ instrument(:exist?, key) do |payload|
706
+ entry = read_entry(key, **options, event: payload)
534
707
  (entry && !entry.expired? && !entry.mismatched?(normalize_version(name, options))) || false
535
708
  end
536
709
  end
@@ -566,7 +739,7 @@ module ActiveSupport
566
739
  raise NotImplementedError.new("#{self.class.name} does not support decrement")
567
740
  end
568
741
 
569
- # Cleanups the cache by removing expired entries.
742
+ # Cleans up the cache by removing expired entries.
570
743
  #
571
744
  # Options are passed to the underlying cache implementation.
572
745
  #
@@ -586,8 +759,15 @@ module ActiveSupport
586
759
  end
587
760
 
588
761
  private
589
- def default_coder
590
- Coders[Cache.format_version]
762
+ def default_serializer
763
+ case Cache.format_version
764
+ when 7.0
765
+ Cache::SerializerWithFallback[:marshal_7_0]
766
+ when 7.1
767
+ Cache::SerializerWithFallback[:marshal_7_1]
768
+ else
769
+ raise ArgumentError, "Unrecognized ActiveSupport::Cache.format_version: #{Cache.format_version.inspect}"
770
+ end
591
771
  end
592
772
 
593
773
  # Adds the namespace defined in the options to a pattern designed to
@@ -624,14 +804,16 @@ module ActiveSupport
624
804
  def serialize_entry(entry, **options)
625
805
  options = merged_options(options)
626
806
  if @coder_supports_compression && options[:compress]
627
- @coder.dump_compressed(entry, options[:compress_threshold] || DEFAULT_COMPRESS_LIMIT)
807
+ @coder.dump_compressed(entry, options[:compress_threshold])
628
808
  else
629
809
  @coder.dump(entry)
630
810
  end
631
811
  end
632
812
 
633
- def deserialize_entry(payload)
813
+ def deserialize_entry(payload, **)
634
814
  payload.nil? ? nil : @coder.load(payload)
815
+ rescue DeserializationError
816
+ nil
635
817
  end
636
818
 
637
819
  # Reads multiple entries from the cache implementation. Subclasses MAY
@@ -677,6 +859,22 @@ module ActiveSupport
677
859
  def merged_options(call_options)
678
860
  if call_options
679
861
  call_options = normalize_options(call_options)
862
+ if call_options.key?(:expires_in) && call_options.key?(:expires_at)
863
+ raise ArgumentError, "Either :expires_in or :expires_at can be supplied, but not both"
864
+ end
865
+
866
+ expires_at = call_options.delete(:expires_at)
867
+ call_options[:expires_in] = (expires_at - Time.now) if expires_at
868
+
869
+ if call_options[:expires_in].is_a?(Time)
870
+ expires_in = call_options[:expires_in]
871
+ raise ArgumentError.new("expires_in parameter should not be a Time. Did you mean to use expires_at? Got: #{expires_in}")
872
+ end
873
+ if call_options[:expires_in]&.negative?
874
+ expires_in = call_options.delete(:expires_in)
875
+ handle_invalid_expires_in("Cache expiration time is invalid, cannot be negative: #{expires_in}")
876
+ end
877
+
680
878
  if options.empty?
681
879
  call_options
682
880
  else
@@ -687,6 +885,16 @@ module ActiveSupport
687
885
  end
688
886
  end
689
887
 
888
+ def handle_invalid_expires_in(message)
889
+ error = ArgumentError.new(message)
890
+ if ActiveSupport::Cache::Store.raise_on_invalid_cache_expiration_time
891
+ raise error
892
+ else
893
+ ActiveSupport.error_reporter&.report(error, handled: true, severity: :warning)
894
+ logger.error("#{error.class}: #{error.message}") if logger
895
+ end
896
+ end
897
+
690
898
  # Normalize aliased options to their canonical form
691
899
  def normalize_options(options)
692
900
  options = options.dup
@@ -699,10 +907,31 @@ module ActiveSupport
699
907
  options
700
908
  end
701
909
 
702
- # Expands and namespaces the cache key. May be overridden by
703
- # cache stores to do additional normalization.
910
+ def validate_options(options)
911
+ if options.key?(:coder) && options[:serializer]
912
+ raise ArgumentError, "Cannot specify :serializer and :coder options together"
913
+ end
914
+
915
+ if options.key?(:coder) && options[:compressor]
916
+ raise ArgumentError, "Cannot specify :compressor and :coder options together"
917
+ end
918
+
919
+ if Cache.format_version < 7.1 && !options[:serializer] && options[:compressor]
920
+ raise ArgumentError, "Cannot specify :compressor option when using" \
921
+ " default serializer and cache format version is < 7.1"
922
+ end
923
+
924
+ options
925
+ end
926
+
927
+ # Expands and namespaces the cache key.
928
+ # Raises an exception when the key is +nil+ or an empty string.
929
+ # May be overridden by cache stores to do additional normalization.
704
930
  def normalize_key(key, options = nil)
705
- namespace_key expanded_key(key), options
931
+ str_key = expanded_key(key)
932
+ raise(ArgumentError, "key cannot be blank") if !str_key || str_key.empty?
933
+
934
+ namespace_key str_key, options
706
935
  end
707
936
 
708
937
  # Prefix the key with a namespace string:
@@ -765,14 +994,33 @@ module ActiveSupport
765
994
  end
766
995
  end
767
996
 
768
- def instrument(operation, key, options = nil)
997
+ def instrument(operation, key, options = nil, &block)
998
+ _instrument(operation, key: key, options: options, &block)
999
+ end
1000
+
1001
+ def instrument_multi(operation, keys, options = nil, &block)
1002
+ _instrument(operation, multi: true, key: keys, options: options, &block)
1003
+ end
1004
+
1005
+ def _instrument(operation, multi: false, options: nil, **payload, &block)
769
1006
  if logger && logger.debug? && !silence?
770
- logger.debug "Cache #{operation}: #{normalize_key(key, options)}#{options.blank? ? "" : " (#{options.inspect})"}"
1007
+ debug_key =
1008
+ if multi
1009
+ ": #{payload[:key].size} key(s) specified"
1010
+ elsif payload[:key]
1011
+ ": #{payload[:key]}"
1012
+ end
1013
+
1014
+ debug_options = " (#{options.inspect})" unless options.blank?
1015
+
1016
+ logger.debug "Cache #{operation}#{debug_key}#{debug_options}"
771
1017
  end
772
1018
 
773
- payload = { key: key, store: self.class.name }
1019
+ payload[:store] = self.class.name
774
1020
  payload.merge!(options) if options.is_a?(Hash)
775
- ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload) { yield(payload) }
1021
+ ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload) do
1022
+ block&.call(payload)
1023
+ end
776
1024
  end
777
1025
 
778
1026
  def handle_expired_entry(entry, key, options)
@@ -782,7 +1030,8 @@ module ActiveSupport
782
1030
  # When an entry has a positive :race_condition_ttl defined, put the stale entry back into the cache
783
1031
  # for a brief period while the entry is being recalculated.
784
1032
  entry.expires_at = Time.now.to_f + race_ttl
785
- write_entry(key, entry, expires_in: race_ttl * 2)
1033
+ options[:expires_in] = race_ttl * 2
1034
+ write_entry(key, entry, **options)
786
1035
  else
787
1036
  delete_entry(key, **options)
788
1037
  end
@@ -792,13 +1041,15 @@ module ActiveSupport
792
1041
  end
793
1042
 
794
1043
  def get_entry_value(entry, name, options)
795
- instrument(:fetch_hit, name, options) { }
1044
+ instrument(:fetch_hit, name, options)
796
1045
  entry.value
797
1046
  end
798
1047
 
799
- def save_block_result_to_cache(name, options)
800
- result = instrument(:generate, name, options) do
801
- yield(name)
1048
+ def save_block_result_to_cache(name, key, options)
1049
+ options = options.dup
1050
+
1051
+ result = instrument(:generate, key, options) do
1052
+ yield(name, WriteOptions.new(options))
802
1053
  end
803
1054
 
804
1055
  write(name, result, options) unless result.nil? && options[:skip_nil]
@@ -806,217 +1057,46 @@ module ActiveSupport
806
1057
  end
807
1058
  end
808
1059
 
809
- module NullCoder # :nodoc:
810
- extend self
811
-
812
- def dump(entry)
813
- entry
814
- end
815
-
816
- def dump_compressed(entry, threshold)
817
- entry.compressed(threshold)
818
- end
819
-
820
- def load(payload)
821
- payload
822
- end
823
- end
824
-
825
- module Coders # :nodoc:
826
- MARK_61 = "\x04\b".b.freeze # The one set by Marshal.
827
- MARK_70_UNCOMPRESSED = "\x00".b.freeze
828
- MARK_70_COMPRESSED = "\x01".b.freeze
829
-
830
- class << self
831
- def [](version)
832
- case version
833
- when 6.1
834
- Rails61Coder
835
- when 7.0
836
- Rails70Coder
837
- else
838
- raise ArgumentError, "Unknown ActiveSupport::Cache.format_version #{Cache.format_version.inspect}"
839
- end
840
- end
841
- end
842
-
843
- module Loader
844
- extend self
845
-
846
- def load(payload)
847
- if !payload.is_a?(String)
848
- ActiveSupport::Cache::Store.logger&.warn %{Payload wasn't a string, was #{payload.class.name} - couldn't unmarshal, so returning nil."}
849
-
850
- return nil
851
- elsif payload.start_with?(MARK_70_UNCOMPRESSED)
852
- members = Marshal.load(payload.byteslice(1..-1))
853
- elsif payload.start_with?(MARK_70_COMPRESSED)
854
- members = Marshal.load(Zlib::Inflate.inflate(payload.byteslice(1..-1)))
855
- elsif payload.start_with?(MARK_61)
856
- return Marshal.load(payload)
857
- else
858
- ActiveSupport::Cache::Store.logger&.warn %{Invalid cache prefix: #{payload.byteslice(0).inspect}, expected "\\x00" or "\\x01"}
859
-
860
- return nil
861
- end
862
- Entry.unpack(members)
863
- end
864
- end
865
-
866
- module Rails61Coder
867
- include Loader
868
- extend self
869
-
870
- def dump(entry)
871
- Marshal.dump(entry)
872
- end
873
-
874
- def dump_compressed(entry, threshold)
875
- Marshal.dump(entry.compressed(threshold))
876
- end
877
- end
878
-
879
- module Rails70Coder
880
- include Loader
881
- extend self
882
-
883
- def dump(entry)
884
- MARK_70_UNCOMPRESSED + Marshal.dump(entry.pack)
885
- end
886
-
887
- def dump_compressed(entry, threshold)
888
- payload = Marshal.dump(entry.pack)
889
- if payload.bytesize >= threshold
890
- compressed_payload = Zlib::Deflate.deflate(payload)
891
- if compressed_payload.bytesize < payload.bytesize
892
- return MARK_70_COMPRESSED + compressed_payload
893
- end
894
- end
895
-
896
- MARK_70_UNCOMPRESSED + payload
897
- end
898
- end
899
- end
900
-
901
- # This class is used to represent cache entries. Cache entries have a value, an optional
902
- # expiration time, and an optional version. The expiration time is used to support the :race_condition_ttl option
903
- # on the cache. The version is used to support the :version option on the cache for rejecting
904
- # mismatches.
905
- #
906
- # Since cache entries in most instances will be serialized, the internals of this class are highly optimized
907
- # using short instance variable names that are lazily defined.
908
- class Entry # :nodoc:
909
- class << self
910
- def unpack(members)
911
- new(members[0], expires_at: members[1], version: members[2])
912
- end
1060
+ # Enables the dynamic configuration of Cache entry options while ensuring
1061
+ # that conflicting options are not both set. When a block is given to
1062
+ # ActiveSupport::Cache::Store#fetch, the second argument will be an
1063
+ # instance of +WriteOptions+.
1064
+ class WriteOptions
1065
+ def initialize(options) # :nodoc:
1066
+ @options = options
913
1067
  end
914
1068
 
915
- attr_reader :version
916
-
917
- # Creates a new cache entry for the specified value. Options supported are
918
- # +:compressed+, +:version+, +:expires_at+ and +:expires_in+.
919
- def initialize(value, compressed: false, version: nil, expires_in: nil, expires_at: nil, **)
920
- @value = value
921
- @version = version
922
- @created_at = 0.0
923
- @expires_in = expires_at&.to_f || expires_in && (expires_in.to_f + Time.now.to_f)
924
- @compressed = true if compressed
1069
+ def version
1070
+ @options[:version]
925
1071
  end
926
1072
 
927
- def value
928
- compressed? ? uncompress(@value) : @value
1073
+ def version=(version)
1074
+ @options[:version] = version
929
1075
  end
930
1076
 
931
- def mismatched?(version)
932
- @version && version && @version != version
1077
+ def expires_in
1078
+ @options[:expires_in]
933
1079
  end
934
1080
 
935
- # Checks if the entry is expired. The +expires_in+ parameter can override
936
- # the value set when the entry was created.
937
- def expired?
938
- @expires_in && @created_at + @expires_in <= Time.now.to_f
1081
+ # Sets the Cache entry's +expires_in+ value. If an +expires_at+ option was
1082
+ # previously set, this will unset it since +expires_in+ and +expires_at+
1083
+ # cannot both be set.
1084
+ def expires_in=(expires_in)
1085
+ @options.delete(:expires_at)
1086
+ @options[:expires_in] = expires_in
939
1087
  end
940
1088
 
941
1089
  def expires_at
942
- @expires_in ? @created_at + @expires_in : nil
943
- end
944
-
945
- def expires_at=(value)
946
- if value
947
- @expires_in = value.to_f - @created_at
948
- else
949
- @expires_in = nil
950
- end
951
- end
952
-
953
- # Returns the size of the cached value. This could be less than
954
- # <tt>value.bytesize</tt> if the data is compressed.
955
- def bytesize
956
- case value
957
- when NilClass
958
- 0
959
- when String
960
- @value.bytesize
961
- else
962
- @s ||= Marshal.dump(@value).bytesize
963
- end
964
- end
965
-
966
- def compressed? # :nodoc:
967
- defined?(@compressed)
968
- end
969
-
970
- def compressed(compress_threshold)
971
- return self if compressed?
972
-
973
- case @value
974
- when nil, true, false, Numeric
975
- uncompressed_size = 0
976
- when String
977
- uncompressed_size = @value.bytesize
978
- else
979
- serialized = Marshal.dump(@value)
980
- uncompressed_size = serialized.bytesize
981
- end
982
-
983
- if uncompressed_size >= compress_threshold
984
- serialized ||= Marshal.dump(@value)
985
- compressed = Zlib::Deflate.deflate(serialized)
986
-
987
- if compressed.bytesize < uncompressed_size
988
- return Entry.new(compressed, compressed: true, expires_at: expires_at, version: version)
989
- end
990
- end
991
- self
992
- end
993
-
994
- def local?
995
- false
996
- end
997
-
998
- # Duplicates the value in a class. This is used by cache implementations that don't natively
999
- # serialize entries to protect against accidental cache modifications.
1000
- def dup_value!
1001
- if @value && !compressed? && !(@value.is_a?(Numeric) || @value == true || @value == false)
1002
- if @value.is_a?(String)
1003
- @value = @value.dup
1004
- else
1005
- @value = Marshal.load(Marshal.dump(@value))
1006
- end
1007
- end
1090
+ @options[:expires_at]
1008
1091
  end
1009
1092
 
1010
- def pack
1011
- members = [value, expires_at, version]
1012
- members.pop while !members.empty? && members.last.nil?
1013
- members
1093
+ # Sets the Cache entry's +expires_at+ value. If an +expires_in+ option was
1094
+ # previously set, this will unset it since +expires_at+ and +expires_in+
1095
+ # cannot both be set.
1096
+ def expires_at=(expires_at)
1097
+ @options.delete(:expires_in)
1098
+ @options[:expires_at] = expires_at
1014
1099
  end
1015
-
1016
- private
1017
- def uncompress(value)
1018
- Marshal.load(Zlib::Inflate.inflate(value))
1019
- end
1020
1100
  end
1021
1101
  end
1022
1102
  end