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