activesupport 7.1.6 → 8.1.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 (169) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +256 -1133
  3. data/README.rdoc +1 -1
  4. data/lib/active_support/array_inquirer.rb +1 -1
  5. data/lib/active_support/backtrace_cleaner.rb +81 -3
  6. data/lib/active_support/benchmark.rb +21 -0
  7. data/lib/active_support/benchmarkable.rb +3 -2
  8. data/lib/active_support/broadcast_logger.rb +65 -78
  9. data/lib/active_support/cache/file_store.rb +29 -14
  10. data/lib/active_support/cache/mem_cache_store.rb +42 -102
  11. data/lib/active_support/cache/memory_store.rb +11 -6
  12. data/lib/active_support/cache/null_store.rb +2 -2
  13. data/lib/active_support/cache/redis_cache_store.rb +58 -46
  14. data/lib/active_support/cache/serializer_with_fallback.rb +0 -23
  15. data/lib/active_support/cache/strategy/local_cache.rb +72 -27
  16. data/lib/active_support/cache/strategy/local_cache_middleware.rb +7 -7
  17. data/lib/active_support/cache.rb +146 -86
  18. data/lib/active_support/callbacks.rb +102 -126
  19. data/lib/active_support/class_attribute.rb +33 -0
  20. data/lib/active_support/code_generator.rb +9 -0
  21. data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +8 -62
  22. data/lib/active_support/concurrency/share_lock.rb +0 -1
  23. data/lib/active_support/concurrency/thread_monitor.rb +55 -0
  24. data/lib/active_support/configurable.rb +34 -0
  25. data/lib/active_support/configuration_file.rb +15 -6
  26. data/lib/active_support/continuous_integration.rb +145 -0
  27. data/lib/active_support/core_ext/array/conversions.rb +3 -5
  28. data/lib/active_support/core_ext/array.rb +7 -7
  29. data/lib/active_support/core_ext/benchmark.rb +4 -14
  30. data/lib/active_support/core_ext/big_decimal.rb +1 -1
  31. data/lib/active_support/core_ext/class/attribute.rb +26 -19
  32. data/lib/active_support/core_ext/class/subclasses.rb +15 -35
  33. data/lib/active_support/core_ext/class.rb +2 -2
  34. data/lib/active_support/core_ext/date/blank.rb +4 -0
  35. data/lib/active_support/core_ext/date/conversions.rb +2 -2
  36. data/lib/active_support/core_ext/date.rb +5 -5
  37. data/lib/active_support/core_ext/date_and_time/compatibility.rb +1 -9
  38. data/lib/active_support/core_ext/date_time/blank.rb +4 -0
  39. data/lib/active_support/core_ext/date_time/compatibility.rb +3 -5
  40. data/lib/active_support/core_ext/date_time/conversions.rb +4 -6
  41. data/lib/active_support/core_ext/date_time.rb +5 -5
  42. data/lib/active_support/core_ext/digest/uuid.rb +6 -0
  43. data/lib/active_support/core_ext/digest.rb +1 -1
  44. data/lib/active_support/core_ext/enumerable.rb +25 -8
  45. data/lib/active_support/core_ext/erb/util.rb +10 -5
  46. data/lib/active_support/core_ext/file.rb +1 -1
  47. data/lib/active_support/core_ext/hash/deep_merge.rb +1 -0
  48. data/lib/active_support/core_ext/hash/except.rb +0 -12
  49. data/lib/active_support/core_ext/hash/keys.rb +4 -4
  50. data/lib/active_support/core_ext/hash.rb +8 -8
  51. data/lib/active_support/core_ext/integer.rb +3 -3
  52. data/lib/active_support/core_ext/kernel.rb +3 -3
  53. data/lib/active_support/core_ext/module/attr_internal.rb +16 -6
  54. data/lib/active_support/core_ext/module/delegation.rb +20 -163
  55. data/lib/active_support/core_ext/module/deprecation.rb +1 -4
  56. data/lib/active_support/core_ext/module/introspection.rb +3 -0
  57. data/lib/active_support/core_ext/module.rb +11 -11
  58. data/lib/active_support/core_ext/numeric/conversions.rb +3 -3
  59. data/lib/active_support/core_ext/numeric.rb +3 -3
  60. data/lib/active_support/core_ext/object/blank.rb +45 -1
  61. data/lib/active_support/core_ext/object/instance_variables.rb +11 -19
  62. data/lib/active_support/core_ext/object/json.rb +24 -11
  63. data/lib/active_support/core_ext/object/to_query.rb +7 -1
  64. data/lib/active_support/core_ext/object/try.rb +2 -2
  65. data/lib/active_support/core_ext/object/with.rb +5 -3
  66. data/lib/active_support/core_ext/object.rb +13 -13
  67. data/lib/active_support/core_ext/pathname/blank.rb +4 -0
  68. data/lib/active_support/core_ext/pathname.rb +2 -2
  69. data/lib/active_support/core_ext/range/overlap.rb +4 -4
  70. data/lib/active_support/core_ext/range/sole.rb +17 -0
  71. data/lib/active_support/core_ext/range.rb +4 -4
  72. data/lib/active_support/core_ext/securerandom.rb +4 -4
  73. data/lib/active_support/core_ext/string/conversions.rb +1 -1
  74. data/lib/active_support/core_ext/string/filters.rb +4 -4
  75. data/lib/active_support/core_ext/string/multibyte.rb +13 -4
  76. data/lib/active_support/core_ext/string/output_safety.rb +19 -19
  77. data/lib/active_support/core_ext/string.rb +13 -13
  78. data/lib/active_support/core_ext/symbol.rb +1 -1
  79. data/lib/active_support/core_ext/thread/backtrace/location.rb +2 -7
  80. data/lib/active_support/core_ext/time/calculations.rb +25 -30
  81. data/lib/active_support/core_ext/time/compatibility.rb +2 -3
  82. data/lib/active_support/core_ext/time/conversions.rb +2 -2
  83. data/lib/active_support/core_ext/time/zones.rb +1 -1
  84. data/lib/active_support/core_ext/time.rb +5 -5
  85. data/lib/active_support/core_ext.rb +1 -2
  86. data/lib/active_support/current_attributes/test_helper.rb +2 -2
  87. data/lib/active_support/current_attributes.rb +58 -50
  88. data/lib/active_support/delegation.rb +200 -0
  89. data/lib/active_support/dependencies/autoload.rb +0 -12
  90. data/lib/active_support/dependencies/interlock.rb +11 -5
  91. data/lib/active_support/dependencies.rb +6 -2
  92. data/lib/active_support/deprecation/constant_accessor.rb +47 -26
  93. data/lib/active_support/deprecation/proxy_wrappers.rb +9 -12
  94. data/lib/active_support/deprecation/reporting.rb +5 -17
  95. data/lib/active_support/deprecation.rb +8 -5
  96. data/lib/active_support/descendants_tracker.rb +9 -87
  97. data/lib/active_support/duration/iso8601_parser.rb +2 -2
  98. data/lib/active_support/duration/iso8601_serializer.rb +1 -2
  99. data/lib/active_support/duration.rb +25 -16
  100. data/lib/active_support/editor.rb +70 -0
  101. data/lib/active_support/encrypted_configuration.rb +20 -2
  102. data/lib/active_support/encrypted_file.rb +1 -1
  103. data/lib/active_support/error_reporter.rb +121 -6
  104. data/lib/active_support/event_reporter/test_helper.rb +32 -0
  105. data/lib/active_support/event_reporter.rb +592 -0
  106. data/lib/active_support/evented_file_update_checker.rb +5 -3
  107. data/lib/active_support/execution_context.rb +64 -7
  108. data/lib/active_support/execution_wrapper.rb +1 -2
  109. data/lib/active_support/file_update_checker.rb +9 -7
  110. data/lib/active_support/fork_tracker.rb +2 -38
  111. data/lib/active_support/gem_version.rb +2 -2
  112. data/lib/active_support/gzip.rb +1 -0
  113. data/lib/active_support/hash_with_indifferent_access.rb +66 -45
  114. data/lib/active_support/html_safe_translation.rb +3 -0
  115. data/lib/active_support/i18n_railtie.rb +19 -11
  116. data/lib/active_support/inflector/inflections.rb +31 -15
  117. data/lib/active_support/inflector/transliterate.rb +6 -8
  118. data/lib/active_support/isolated_execution_state.rb +12 -17
  119. data/lib/active_support/json/decoding.rb +6 -4
  120. data/lib/active_support/json/encoding.rb +157 -21
  121. data/lib/active_support/lazy_load_hooks.rb +1 -1
  122. data/lib/active_support/log_subscriber.rb +2 -18
  123. data/lib/active_support/logger.rb +15 -2
  124. data/lib/active_support/logger_thread_safe_level.rb +4 -9
  125. data/lib/active_support/message_encryptors.rb +54 -2
  126. data/lib/active_support/message_pack/extensions.rb +20 -2
  127. data/lib/active_support/message_verifier.rb +21 -0
  128. data/lib/active_support/message_verifiers.rb +57 -3
  129. data/lib/active_support/messages/rotation_coordinator.rb +9 -0
  130. data/lib/active_support/messages/rotator.rb +10 -0
  131. data/lib/active_support/multibyte/chars.rb +14 -4
  132. data/lib/active_support/multibyte.rb +4 -0
  133. data/lib/active_support/notifications/fanout.rb +68 -50
  134. data/lib/active_support/notifications/instrumenter.rb +22 -19
  135. data/lib/active_support/notifications.rb +28 -27
  136. data/lib/active_support/number_helper/number_converter.rb +2 -2
  137. data/lib/active_support/number_helper.rb +22 -0
  138. data/lib/active_support/option_merger.rb +2 -2
  139. data/lib/active_support/ordered_options.rb +53 -15
  140. data/lib/active_support/railtie.rb +36 -20
  141. data/lib/active_support/string_inquirer.rb +1 -1
  142. data/lib/active_support/structured_event_subscriber.rb +99 -0
  143. data/lib/active_support/subscriber.rb +1 -5
  144. data/lib/active_support/syntax_error_proxy.rb +3 -0
  145. data/lib/active_support/tagged_logging.rb +5 -1
  146. data/lib/active_support/test_case.rb +63 -6
  147. data/lib/active_support/testing/assertions.rb +113 -27
  148. data/lib/active_support/testing/constant_stubbing.rb +30 -8
  149. data/lib/active_support/testing/deprecation.rb +5 -12
  150. data/lib/active_support/testing/error_reporter_assertions.rb +18 -1
  151. data/lib/active_support/testing/event_reporter_assertions.rb +227 -0
  152. data/lib/active_support/testing/isolation.rb +19 -9
  153. data/lib/active_support/testing/method_call_assertions.rb +2 -16
  154. data/lib/active_support/testing/notification_assertions.rb +92 -0
  155. data/lib/active_support/testing/parallelization/server.rb +18 -2
  156. data/lib/active_support/testing/parallelization/worker.rb +4 -2
  157. data/lib/active_support/testing/parallelization.rb +25 -1
  158. data/lib/active_support/testing/tests_without_assertions.rb +19 -0
  159. data/lib/active_support/testing/time_helpers.rb +11 -6
  160. data/lib/active_support/time_with_zone.rb +39 -26
  161. data/lib/active_support/values/time_zone.rb +26 -17
  162. data/lib/active_support/xml_mini.rb +14 -4
  163. data/lib/active_support.rb +22 -9
  164. metadata +31 -17
  165. data/lib/active_support/core_ext/range/each.rb +0 -24
  166. data/lib/active_support/deprecation/instance_delegator.rb +0 -65
  167. data/lib/active_support/proxy_object.rb +0 -17
  168. data/lib/active_support/ruby_features.rb +0 -7
  169. data/lib/active_support/testing/strict_warnings.rb +0 -39
@@ -41,47 +41,6 @@ module ActiveSupport
41
41
 
42
42
  prepend Strategy::LocalCache
43
43
 
44
- module DupLocalCache
45
- class DupLocalStore < DelegateClass(Strategy::LocalCache::LocalStore)
46
- def write_entry(_key, entry)
47
- if entry.is_a?(Entry)
48
- entry.dup_value!
49
- end
50
- super
51
- end
52
-
53
- def fetch_entry(key)
54
- entry = super do
55
- new_entry = yield
56
- if entry.is_a?(Entry)
57
- new_entry.dup_value!
58
- end
59
- new_entry
60
- end
61
- entry = entry.dup
62
-
63
- if entry.is_a?(Entry)
64
- entry.dup_value!
65
- end
66
-
67
- entry
68
- end
69
- end
70
-
71
- private
72
- def local_cache
73
- if ActiveSupport::Cache.format_version == 6.1
74
- if local_cache = super
75
- DupLocalStore.new(local_cache)
76
- end
77
- else
78
- super
79
- end
80
- end
81
- end
82
- prepend DupLocalCache
83
-
84
- KEY_MAX_SIZE = 250
85
44
  ESCAPE_KEY_CHARS = /[\x00-\x20%\x7F-\xFF]/n
86
45
 
87
46
  # Creates a new Dalli::Client instance with specified addresses and options.
@@ -114,31 +73,24 @@ module ActiveSupport
114
73
  #
115
74
  # If no addresses are provided, but <tt>ENV['MEMCACHE_SERVERS']</tt> is defined, it will be used instead. Otherwise,
116
75
  # +MemCacheStore+ will connect to localhost:11211 (the default memcached port).
117
- # Passing a +Dalli::Client+ instance is deprecated and will be removed. Please pass an address instead.
118
76
  def initialize(*addresses)
119
77
  addresses = addresses.flatten
120
78
  options = addresses.extract_options!
121
79
  if options.key?(:cache_nils)
122
80
  options[:skip_nil] = !options.delete(:cache_nils)
123
81
  end
82
+ options[:max_key_size] ||= MAX_KEY_SIZE
124
83
  super(options)
125
84
 
126
85
  unless [String, Dalli::Client, NilClass].include?(addresses.first.class)
127
86
  raise ArgumentError, "First argument must be an empty array, address, or array of addresses."
128
87
  end
129
- if addresses.first.is_a?(Dalli::Client)
130
- ActiveSupport.deprecator.warn(<<~MSG)
131
- Initializing MemCacheStore with a Dalli::Client is deprecated and will be removed in Rails 7.2.
132
- Use memcached server addresses instead.
133
- MSG
134
- @data = addresses.first
135
- else
136
- @mem_cache_options = options.dup
137
- # The value "compress: false" prevents duplicate compression within Dalli.
138
- @mem_cache_options[:compress] = false
139
- (OVERRIDDEN_OPTIONS - %i(compress)).each { |name| @mem_cache_options.delete(name) }
140
- @data = self.class.build_mem_cache(*(addresses + [@mem_cache_options]))
141
- end
88
+
89
+ @mem_cache_options = options.dup
90
+ # The value "compress: false" prevents duplicate compression within Dalli.
91
+ @mem_cache_options[:compress] = false
92
+ (OVERRIDDEN_OPTIONS - %i(compress)).each { |name| @mem_cache_options.delete(name) }
93
+ @data = self.class.build_mem_cache(*(addresses + [@mem_cache_options]))
142
94
  end
143
95
 
144
96
  def inspect
@@ -158,9 +110,6 @@ module ActiveSupport
158
110
  # * <tt>raw: true</tt> - Sends the value directly to the server as raw
159
111
  # bytes. The value must be a string or number. You can use memcached
160
112
  # direct operations like +increment+ and +decrement+ only on raw values.
161
- #
162
- # * <tt>unless_exist: true</tt> - Prevents overwriting an existing cache
163
- # entry.
164
113
 
165
114
  # Increment a cached integer value using the memcached incr atomic operator.
166
115
  # Returns the updated value.
@@ -177,11 +126,18 @@ module ActiveSupport
177
126
  #
178
127
  # Incrementing a non-numeric value, or a value written without
179
128
  # <tt>raw: true</tt>, will fail and return +nil+.
129
+ #
130
+ # To read the value later, call #read_counter:
131
+ #
132
+ # cache.increment("baz") # => 7
133
+ # cache.read_counter("baz") # 7
180
134
  def increment(name, amount = 1, options = nil)
181
135
  options = merged_options(options)
182
- instrument(:increment, name, amount: amount) do
136
+ key = normalize_key(name, options)
137
+
138
+ instrument(:increment, key, amount: amount) do
183
139
  rescue_error_with nil do
184
- @data.with { |c| c.incr(normalize_key(name, options), amount, options[:expires_in], amount) }
140
+ @data.with { |c| c.incr(key, amount, options[:expires_in], amount) }
185
141
  end
186
142
  end
187
143
  end
@@ -201,11 +157,18 @@ module ActiveSupport
201
157
  #
202
158
  # Decrementing a non-numeric value, or a value written without
203
159
  # <tt>raw: true</tt>, will fail and return +nil+.
160
+ #
161
+ # To read the value later, call #read_counter:
162
+ #
163
+ # cache.decrement("baz") # => 3
164
+ # cache.read_counter("baz") # 3
204
165
  def decrement(name, amount = 1, options = nil)
205
166
  options = merged_options(options)
206
- instrument(:decrement, name, amount: amount) do
167
+ key = normalize_key(name, options)
168
+
169
+ instrument(:decrement, key, amount: amount) do
207
170
  rescue_error_with nil do
208
- @data.with { |c| c.decr(normalize_key(name, options), amount, options[:expires_in], 0) }
171
+ @data.with { |c| c.decr(key, amount, options[:expires_in], 0) }
209
172
  end
210
173
  end
211
174
  end
@@ -222,20 +185,6 @@ module ActiveSupport
222
185
  end
223
186
 
224
187
  private
225
- def default_serializer
226
- if Cache.format_version == 6.1
227
- ActiveSupport.deprecator.warn <<~EOM
228
- Support for `config.active_support.cache_format_version = 6.1` has been deprecated and will be removed in Rails 7.2.
229
-
230
- Check the Rails upgrade guide at https://guides.rubyonrails.org/upgrading_ruby_on_rails.html#new-activesupport-cache-serialization-format
231
- for more information on how to upgrade.
232
- EOM
233
- Cache::SerializerWithFallback[:passthrough]
234
- else
235
- super
236
- end
237
- end
238
-
239
188
  # Read an entry from the cache.
240
189
  def read_entry(key, **options)
241
190
  deserialize_entry(read_serialized_entry(key, **options), **options)
@@ -259,10 +208,10 @@ module ActiveSupport
259
208
  # Set the memcache expire a few minutes in the future to support race condition ttls on read
260
209
  expires_in += 5.minutes
261
210
  end
262
- rescue_error_with false do
211
+ rescue_error_with nil do
263
212
  # Don't pass compress option to Dalli since we are already dealing with compression.
264
213
  options.delete(:compress)
265
- @data.with { |c| c.send(method, key, payload, expires_in, **options) }
214
+ @data.with { |c| !!c.send(method, key, payload, expires_in, **options) }
266
215
  end
267
216
  end
268
217
 
@@ -270,26 +219,24 @@ module ActiveSupport
270
219
  def read_multi_entries(names, **options)
271
220
  keys_to_names = names.index_by { |name| normalize_key(name, options) }
272
221
 
273
- raw_values = begin
274
- @data.with { |c| c.get_multi(keys_to_names.keys) }
275
- rescue Dalli::UnmarshalError
276
- {}
277
- end
222
+ rescue_error_with({}) do
223
+ raw_values = @data.with { |c| c.get_multi(keys_to_names.keys) }
278
224
 
279
- values = {}
225
+ values = {}
280
226
 
281
- raw_values.each do |key, value|
282
- entry = deserialize_entry(value, raw: options[:raw])
227
+ raw_values.each do |key, value|
228
+ entry = deserialize_entry(value, raw: options[:raw])
283
229
 
284
- unless entry.nil? || entry.expired? || entry.mismatched?(normalize_version(keys_to_names[key], options))
285
- begin
286
- values[keys_to_names[key]] = entry.value
287
- rescue DeserializationError
230
+ unless entry.nil? || entry.expired? || entry.mismatched?(normalize_version(keys_to_names[key], options))
231
+ begin
232
+ values[keys_to_names[key]] = entry.value
233
+ rescue DeserializationError
234
+ end
288
235
  end
289
236
  end
290
- end
291
237
 
292
- values
238
+ values
239
+ end
293
240
  end
294
241
 
295
242
  # Delete an entry from the cache.
@@ -309,19 +256,12 @@ module ActiveSupport
309
256
  # before applying the regular expression to ensure we are escaping all
310
257
  # characters properly.
311
258
  def normalize_key(key, options)
312
- key = super
259
+ key = expand_and_namespace_key(key, options)
313
260
  if key
314
261
  key = key.dup.force_encoding(Encoding::ASCII_8BIT)
315
262
  key = key.gsub(ESCAPE_KEY_CHARS) { |match| "%#{match.getbyte(0).to_s(16).upcase}" }
316
-
317
- if key.size > KEY_MAX_SIZE
318
- key_separator = ":hash:"
319
- key_hash = ActiveSupport::Digest.hexdigest(key)
320
- key_trim_size = KEY_MAX_SIZE - key_separator.size - key_hash.size
321
- key = "#{key[0, key_trim_size]}#{key_separator}#{key_hash}"
322
- end
323
263
  end
324
- key
264
+ truncate_key(key)
325
265
  end
326
266
 
327
267
  def deserialize_entry(payload, raw: false, **)
@@ -334,7 +274,7 @@ module ActiveSupport
334
274
 
335
275
  def rescue_error_with(fallback)
336
276
  yield
337
- rescue Dalli::DalliError => error
277
+ rescue Dalli::DalliError, ConnectionPool::Error, ConnectionPool::TimeoutError => error
338
278
  logger.error("DalliError (#{error}): #{error.message}") if logger
339
279
  ActiveSupport.error_reporter&.report(
340
280
  error,
@@ -146,8 +146,10 @@ module ActiveSupport
146
146
  # cache.write("baz", 5)
147
147
  # cache.increment("baz") # => 6
148
148
  #
149
- def increment(name, amount = 1, options = nil)
150
- modify_value(name, amount, options)
149
+ def increment(name, amount = 1, **options)
150
+ instrument(:increment, name, amount: amount) do
151
+ modify_value(name, amount, **options)
152
+ end
151
153
  end
152
154
 
153
155
  # Decrement a cached integer value. Returns the updated value.
@@ -161,15 +163,18 @@ module ActiveSupport
161
163
  # cache.write("baz", 5)
162
164
  # cache.decrement("baz") # => 4
163
165
  #
164
- def decrement(name, amount = 1, options = nil)
165
- modify_value(name, -amount, options)
166
+ def decrement(name, amount = 1, **options)
167
+ instrument(:decrement, name, amount: amount) do
168
+ modify_value(name, -amount, **options)
169
+ end
166
170
  end
167
171
 
168
172
  # Deletes cache entries if the cache key matches a given pattern.
169
173
  def delete_matched(matcher, options = nil)
170
174
  options = merged_options(options)
175
+ matcher = key_matcher(matcher, options)
176
+
171
177
  instrument(:delete_matched, matcher.inspect) do
172
- matcher = key_matcher(matcher, options)
173
178
  keys = synchronize { @data.keys }
174
179
  keys.each do |key|
175
180
  delete_entry(key, **options) if key.match(matcher)
@@ -233,7 +238,7 @@ module ActiveSupport
233
238
 
234
239
  # Modifies the amount of an integer value that is stored in the cache.
235
240
  # If the key is not found it is created and set to +amount+.
236
- def modify_value(name, amount, options)
241
+ def modify_value(name, amount, **options)
237
242
  options = merged_options(options)
238
243
  key = normalize_key(name, options)
239
244
  version = normalize_version(name, options)
@@ -25,10 +25,10 @@ module ActiveSupport
25
25
  def cleanup(options = nil)
26
26
  end
27
27
 
28
- def increment(name, amount = 1, options = nil)
28
+ def increment(name, amount = 1, **options)
29
29
  end
30
30
 
31
- def decrement(name, amount = 1, options = nil)
31
+ def decrement(name, amount = 1, **options)
32
32
  end
33
33
 
34
34
  def delete_matched(matcher, options = nil)
@@ -35,9 +35,6 @@ module ActiveSupport
35
35
  # +Redis::Distributed+ 4.0.1+ for distributed mget support.
36
36
  # * +delete_matched+ support for Redis KEYS globs.
37
37
  class RedisCacheStore < Store
38
- # Keys are truncated with the Active Support digest if they exceed 1kB
39
- MAX_KEY_BYTESIZE = 1024
40
-
41
38
  DEFAULT_REDIS_OPTIONS = {
42
39
  connect_timeout: 1,
43
40
  read_timeout: 1,
@@ -106,20 +103,29 @@ module ActiveSupport
106
103
  end
107
104
  end
108
105
 
109
- attr_reader :max_key_bytesize
110
106
  attr_reader :redis
111
107
 
112
108
  # Creates a new Redis cache store.
113
109
  #
114
- # There are four ways to provide the Redis client used by the cache: the
115
- # +:redis+ param can be a Redis instance or a block that returns a Redis
116
- # instance, or the +:url+ param can be a string or an array of strings
117
- # which will be used to create a Redis instance or a +Redis::Distributed+
118
- # instance.
110
+ # There are a few ways to provide the Redis client used by the cache:
111
+ #
112
+ # 1. The +:redis+ param can be:
113
+ # - A Redis instance.
114
+ # - A +ConnectionPool+ instance wrapping a Redis instance.
115
+ # - A block that returns a Redis instance.
116
+ #
117
+ # 2. The +:url+ param can be:
118
+ # - A string used to create a Redis instance.
119
+ # - An array of strings used to create a +Redis::Distributed+ instance.
120
+ #
121
+ # If the final Redis instance is not already a +ConnectionPool+, it will
122
+ # be wrapped in one using +ActiveSupport::Cache::Store::DEFAULT_POOL_OPTIONS+.
123
+ # These options can be overridden with the +:pool+ param, or the pool can be
124
+ # disabled with +:pool: false+.
119
125
  #
120
126
  # Option Class Result
121
- # :redis Proc -> options[:redis].call
122
127
  # :redis Object -> options[:redis]
128
+ # :redis Proc -> options[:redis].call
123
129
  # :url String -> Redis.new(url: …)
124
130
  # :url Array -> Redis::Distributed.new([{ url: … }, { url: … }, …])
125
131
  #
@@ -148,14 +154,17 @@ module ActiveSupport
148
154
  # cache.exist?('bar') # => false
149
155
  def initialize(error_handler: DEFAULT_ERROR_HANDLER, **redis_options)
150
156
  universal_options = redis_options.extract!(*UNIVERSAL_OPTIONS)
157
+ redis = redis_options[:redis]
151
158
 
152
- if pool_options = self.class.send(:retrieve_pool_options, redis_options)
159
+ already_pool = redis.instance_of?(::ConnectionPool) ||
160
+ (redis.respond_to?(:wrapped_pool) && redis.wrapped_pool.instance_of?(::ConnectionPool))
161
+
162
+ if !already_pool && pool_options = self.class.send(:retrieve_pool_options, redis_options)
153
163
  @redis = ::ConnectionPool.new(pool_options) { self.class.build_redis(**redis_options) }
154
164
  else
155
165
  @redis = self.class.build_redis(**redis_options)
156
166
  end
157
167
 
158
- @max_key_bytesize = MAX_KEY_BYTESIZE
159
168
  @error_handler = error_handler
160
169
 
161
170
  super(universal_options)
@@ -173,9 +182,12 @@ module ActiveSupport
173
182
  return {} if names.empty?
174
183
 
175
184
  options = names.extract_options!
176
- instrument_multi(:read_multi, names, options) do |payload|
185
+ options = merged_options(options)
186
+ keys = names.map { |name| normalize_key(name, options) }
187
+
188
+ instrument_multi(:read_multi, keys, options) do |payload|
177
189
  read_multi_entries(names, **options).tap do |results|
178
- payload[:hits] = results.keys
190
+ payload[:hits] = results.keys.map { |name| normalize_key(name, options) }
179
191
  end
180
192
  end
181
193
  end
@@ -196,12 +208,13 @@ module ActiveSupport
196
208
  #
197
209
  # Failsafe: Raises errors.
198
210
  def delete_matched(matcher, options = nil)
199
- instrument :delete_matched, matcher do
200
- unless String === matcher
201
- raise ArgumentError, "Only Redis glob strings are supported: #{matcher.inspect}"
202
- end
211
+ unless String === matcher
212
+ raise ArgumentError, "Only Redis glob strings are supported: #{matcher.inspect}"
213
+ end
214
+ pattern = namespace_key(matcher, options)
215
+
216
+ instrument :delete_matched, pattern do
203
217
  redis.then do |c|
204
- pattern = namespace_key(matcher, options)
205
218
  cursor = "0"
206
219
  # Fetch keys in batches using SCAN to avoid blocking the Redis server.
207
220
  nodes = c.respond_to?(:nodes) ? c.nodes : [c]
@@ -209,7 +222,7 @@ module ActiveSupport
209
222
  nodes.each do |node|
210
223
  begin
211
224
  cursor, keys = node.scan(cursor, match: pattern, count: SCAN_BATCH_SIZE)
212
- node.del(*keys) unless keys.empty?
225
+ node.unlink(*keys) unless keys.empty?
213
226
  end until cursor == "0"
214
227
  end
215
228
  end
@@ -232,12 +245,18 @@ module ActiveSupport
232
245
  # Incrementing a non-numeric value, or a value written without
233
246
  # <tt>raw: true</tt>, will fail and return +nil+.
234
247
  #
248
+ # To read the value later, call #read_counter:
249
+ #
250
+ # cache.increment("baz") # => 7
251
+ # cache.read_counter("baz") # 7
252
+ #
235
253
  # Failsafe: Raises errors.
236
254
  def increment(name, amount = 1, options = nil)
237
- instrument :increment, name, amount: amount do
255
+ options = merged_options(options)
256
+ key = normalize_key(name, options)
257
+
258
+ instrument :increment, key, amount: amount do
238
259
  failsafe :increment do
239
- options = merged_options(options)
240
- key = normalize_key(name, options)
241
260
  change_counter(key, amount, options)
242
261
  end
243
262
  end
@@ -258,12 +277,18 @@ module ActiveSupport
258
277
  # Decrementing a non-numeric value, or a value written without
259
278
  # <tt>raw: true</tt>, will fail and return +nil+.
260
279
  #
280
+ # To read the value later, call #read_counter:
281
+ #
282
+ # cache.decrement("baz") # => 3
283
+ # cache.read_counter("baz") # 3
284
+ #
261
285
  # Failsafe: Raises errors.
262
286
  def decrement(name, amount = 1, options = nil)
263
- instrument :decrement, name, amount: amount do
287
+ options = merged_options(options)
288
+ key = normalize_key(name, options)
289
+
290
+ instrument :decrement, key, amount: amount do
264
291
  failsafe :decrement do
265
- options = merged_options(options)
266
- key = normalize_key(name, options)
267
292
  change_counter(key, -amount, options)
268
293
  end
269
294
  end
@@ -369,8 +394,8 @@ module ActiveSupport
369
394
  if pipeline
370
395
  pipeline.set(key, payload, **modifiers)
371
396
  else
372
- failsafe :write_entry, returning: false do
373
- redis.then { |c| c.set key, payload, **modifiers }
397
+ failsafe :write_entry, returning: nil do
398
+ redis.then { |c| !!c.set(key, payload, **modifiers) }
374
399
  end
375
400
  end
376
401
  end
@@ -378,14 +403,16 @@ module ActiveSupport
378
403
  # Delete an entry from the cache.
379
404
  def delete_entry(key, **options)
380
405
  failsafe :delete_entry, returning: false do
381
- redis.then { |c| c.del(key) == 1 }
406
+ redis.then { |c| c.unlink(key) == 1 }
382
407
  end
383
408
  end
384
409
 
385
410
  # Deletes multiple entries in the cache. Returns the number of entries deleted.
386
411
  def delete_multi_entries(entries, **_options)
412
+ return 0 if entries.empty?
413
+
387
414
  failsafe :delete_multi_entries, returning: 0 do
388
- redis.then { |c| c.del(entries) }
415
+ redis.then { |c| c.unlink(*entries) }
389
416
  end
390
417
  end
391
418
 
@@ -404,21 +431,6 @@ module ActiveSupport
404
431
  end
405
432
  end
406
433
 
407
- # Truncate keys that exceed 1kB.
408
- def normalize_key(key, options)
409
- truncate_key super&.b
410
- end
411
-
412
- def truncate_key(key)
413
- if key && key.bytesize > max_key_bytesize
414
- suffix = ":hash:#{ActiveSupport::Digest.hexdigest(key)}"
415
- truncate_at = max_key_bytesize - suffix.bytesize
416
- "#{key.byteslice(0, truncate_at)}#{suffix}"
417
- else
418
- key
419
- end
420
- end
421
-
422
434
  def deserialize_entry(payload, raw: false, **)
423
435
  if raw && !payload.nil?
424
436
  Entry.new(payload)
@@ -477,7 +489,7 @@ module ActiveSupport
477
489
 
478
490
  def failsafe(method, returning: nil)
479
491
  yield
480
- rescue ::Redis::BaseError => error
492
+ rescue ::Redis::BaseError, ConnectionPool::Error, ConnectionPool::TimeoutError => error
481
493
  @error_handler&.call(method: method, exception: error, returning: returning)
482
494
  returning
483
495
  end
@@ -63,28 +63,6 @@ module ActiveSupport
63
63
  end
64
64
  end
65
65
 
66
- module Marshal61WithFallback
67
- include SerializerWithFallback
68
- extend self
69
-
70
- MARSHAL_SIGNATURE = "\x04\x08".b.freeze
71
-
72
- def dump(entry)
73
- Marshal.dump(entry)
74
- end
75
-
76
- def dump_compressed(entry, threshold)
77
- Marshal.dump(entry.compressed(threshold))
78
- end
79
-
80
- alias_method :_load, :marshal_load
81
- public :_load
82
-
83
- def dumped?(dumped)
84
- dumped.start_with?(MARSHAL_SIGNATURE)
85
- end
86
- end
87
-
88
66
  module Marshal70WithFallback
89
67
  include SerializerWithFallback
90
68
  extend self
@@ -165,7 +143,6 @@ module ActiveSupport
165
143
 
166
144
  SERIALIZERS = {
167
145
  passthrough: PassthroughWithFallback,
168
- marshal_6_1: Marshal61WithFallback,
169
146
  marshal_7_0: Marshal70WithFallback,
170
147
  marshal_7_1: Marshal71WithFallback,
171
148
  message_pack: MessagePackWithFallback,
@@ -68,12 +68,25 @@ module ActiveSupport
68
68
  use_temporary_local_cache(LocalStore.new, &block)
69
69
  end
70
70
 
71
+ # Set a new local cache.
72
+ def new_local_cache
73
+ LocalCacheRegistry.set_cache_for(local_cache_key, LocalStore.new)
74
+ end
75
+
76
+ # Unset the current local cache.
77
+ def unset_local_cache
78
+ LocalCacheRegistry.set_cache_for(local_cache_key, nil)
79
+ end
80
+
81
+ # The current local cache.
82
+ def local_cache
83
+ LocalCacheRegistry.cache_for(local_cache_key)
84
+ end
85
+
71
86
  # Middleware class can be inserted as a Rack handler to be local cache for the
72
87
  # duration of request.
73
88
  def middleware
74
- @middleware ||= Middleware.new(
75
- "ActiveSupport::Cache::Strategy::LocalCache",
76
- local_cache_key)
89
+ @middleware ||= Middleware.new("ActiveSupport::Cache::Strategy::LocalCache", self)
77
90
  end
78
91
 
79
92
  def clear(options = nil) # :nodoc:
@@ -94,28 +107,54 @@ module ActiveSupport
94
107
  super
95
108
  end
96
109
 
97
- def increment(name, amount = 1, options = nil) # :nodoc:
110
+ def increment(name, amount = 1, **options) # :nodoc:
98
111
  return super unless local_cache
99
112
  value = bypass_local_cache { super }
100
- if options
101
- write_cache_value(name, value, raw: true, **options)
102
- else
103
- write_cache_value(name, value, raw: true)
104
- end
113
+ write_cache_value(name, value, raw: true, **options)
105
114
  value
106
115
  end
107
116
 
108
- def decrement(name, amount = 1, options = nil) # :nodoc:
117
+ def decrement(name, amount = 1, **options) # :nodoc:
109
118
  return super unless local_cache
110
119
  value = bypass_local_cache { super }
111
- if options
112
- write_cache_value(name, value, raw: true, **options)
113
- else
114
- write_cache_value(name, value, raw: true)
115
- end
120
+ write_cache_value(name, value, raw: true, **options)
116
121
  value
117
122
  end
118
123
 
124
+ def fetch_multi(*names, &block) # :nodoc:
125
+ return super if local_cache.nil? || names.empty?
126
+
127
+ options = names.extract_options!
128
+ options = merged_options(options)
129
+
130
+ keys_to_names = names.index_by { |name| normalize_key(name, options) }
131
+
132
+ local_entries = local_cache.read_multi_entries(keys_to_names.keys)
133
+ results = local_entries.each_with_object({}) do |(key, value), result|
134
+ # If we recorded a miss in the local cache, `#fetch_multi` will forward
135
+ # that key to the real store, and the entry will be replaced
136
+ # local_cache.delete_entry(key)
137
+ next if value.nil?
138
+
139
+ entry = deserialize_entry(value, **options)
140
+
141
+ normalized_key = keys_to_names[key]
142
+ if entry.nil?
143
+ result[normalized_key] = nil
144
+ elsif entry.expired? || entry.mismatched?(normalize_version(normalized_key, options))
145
+ local_cache.delete_entry(key)
146
+ else
147
+ result[normalized_key] = entry.value
148
+ end
149
+ end
150
+
151
+ if results.size < names.size
152
+ results.merge!(super(*(names - results.keys), options, &block))
153
+ end
154
+
155
+ results
156
+ end
157
+
119
158
  private
120
159
  def read_serialized_entry(key, raw: false, **options)
121
160
  if cache = local_cache
@@ -137,17 +176,27 @@ module ActiveSupport
137
176
  keys_to_names = names.index_by { |name| normalize_key(name, options) }
138
177
 
139
178
  local_entries = local_cache.read_multi_entries(keys_to_names.keys)
140
- local_entries.transform_keys! { |key| keys_to_names[key] }
141
- local_entries.transform_values! do |payload|
142
- deserialize_entry(payload, **options)&.value
179
+
180
+ results = local_entries.each_with_object({}) do |(key, value), result|
181
+ next if value.nil? # recorded cache miss
182
+
183
+ entry = deserialize_entry(value, **options)
184
+
185
+ normalized_key = keys_to_names[key]
186
+ if entry.nil?
187
+ result[normalized_key] = nil
188
+ elsif entry.expired? || entry.mismatched?(normalize_version(normalized_key, options))
189
+ local_cache.delete_entry(key)
190
+ else
191
+ result[normalized_key] = entry.value
192
+ end
143
193
  end
144
- missed_names = names - local_entries.keys
145
194
 
146
- if missed_names.any?
147
- local_entries.merge!(super(missed_names, **options))
148
- else
149
- local_entries
195
+ if results.size < names.size
196
+ results.merge!(super(names - results.keys, **options))
150
197
  end
198
+
199
+ results
151
200
  end
152
201
 
153
202
  def write_serialized_entry(key, payload, **)
@@ -178,10 +227,6 @@ module ActiveSupport
178
227
  @local_cache_key ||= "#{self.class.name.underscore}_local_cache_#{object_id}".gsub(/[\/-]/, "_").to_sym
179
228
  end
180
229
 
181
- def local_cache
182
- LocalCacheRegistry.cache_for(local_cache_key)
183
- end
184
-
185
230
  def bypass_local_cache(&block)
186
231
  use_temporary_local_cache(nil, &block)
187
232
  end