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