activesupport 7.0.10 → 7.1.0.beta1

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 (177) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +703 -361
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +4 -4
  5. data/lib/active_support/actionable_error.rb +3 -1
  6. data/lib/active_support/array_inquirer.rb +2 -0
  7. data/lib/active_support/backtrace_cleaner.rb +25 -5
  8. data/lib/active_support/benchmarkable.rb +1 -0
  9. data/lib/active_support/builder.rb +1 -1
  10. data/lib/active_support/cache/coder.rb +153 -0
  11. data/lib/active_support/cache/entry.rb +128 -0
  12. data/lib/active_support/cache/file_store.rb +37 -10
  13. data/lib/active_support/cache/mem_cache_store.rb +84 -68
  14. data/lib/active_support/cache/memory_store.rb +76 -26
  15. data/lib/active_support/cache/null_store.rb +6 -0
  16. data/lib/active_support/cache/redis_cache_store.rb +126 -131
  17. data/lib/active_support/cache/serializer_with_fallback.rb +175 -0
  18. data/lib/active_support/cache/strategy/local_cache.rb +20 -8
  19. data/lib/active_support/cache.rb +304 -246
  20. data/lib/active_support/callbacks.rb +38 -18
  21. data/lib/active_support/concern.rb +4 -2
  22. data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +42 -3
  23. data/lib/active_support/concurrency/null_lock.rb +13 -0
  24. data/lib/active_support/configurable.rb +10 -0
  25. data/lib/active_support/core_ext/array/conversions.rb +2 -1
  26. data/lib/active_support/core_ext/array.rb +0 -1
  27. data/lib/active_support/core_ext/class/attribute.rb +1 -0
  28. data/lib/active_support/core_ext/class/subclasses.rb +13 -10
  29. data/lib/active_support/core_ext/date/conversions.rb +1 -0
  30. data/lib/active_support/core_ext/date.rb +0 -1
  31. data/lib/active_support/core_ext/date_and_time/calculations.rb +10 -0
  32. data/lib/active_support/core_ext/date_time/conversions.rb +6 -2
  33. data/lib/active_support/core_ext/date_time.rb +0 -1
  34. data/lib/active_support/core_ext/digest/uuid.rb +1 -10
  35. data/lib/active_support/core_ext/enumerable.rb +3 -75
  36. data/lib/active_support/core_ext/erb/util.rb +196 -0
  37. data/lib/active_support/core_ext/hash/conversions.rb +1 -1
  38. data/lib/active_support/core_ext/module/attribute_accessors.rb +6 -0
  39. data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +34 -16
  40. data/lib/active_support/core_ext/module/concerning.rb +6 -6
  41. data/lib/active_support/core_ext/module/delegation.rb +40 -11
  42. data/lib/active_support/core_ext/module/deprecation.rb +15 -12
  43. data/lib/active_support/core_ext/module/introspection.rb +0 -1
  44. data/lib/active_support/core_ext/numeric/bytes.rb +9 -0
  45. data/lib/active_support/core_ext/numeric/conversions.rb +2 -0
  46. data/lib/active_support/core_ext/numeric.rb +0 -1
  47. data/lib/active_support/core_ext/object/deep_dup.rb +16 -0
  48. data/lib/active_support/core_ext/object/duplicable.rb +15 -24
  49. data/lib/active_support/core_ext/object/inclusion.rb +13 -5
  50. data/lib/active_support/core_ext/object/instance_variables.rb +22 -12
  51. data/lib/active_support/core_ext/object/json.rb +14 -8
  52. data/lib/active_support/core_ext/object/with.rb +44 -0
  53. data/lib/active_support/core_ext/object/with_options.rb +4 -4
  54. data/lib/active_support/core_ext/object.rb +1 -0
  55. data/lib/active_support/core_ext/pathname/blank.rb +16 -0
  56. data/lib/active_support/core_ext/pathname/existence.rb +2 -0
  57. data/lib/active_support/core_ext/pathname.rb +1 -0
  58. data/lib/active_support/core_ext/range/conversions.rb +28 -7
  59. data/lib/active_support/core_ext/range/overlap.rb +12 -0
  60. data/lib/active_support/core_ext/range.rb +1 -2
  61. data/lib/active_support/core_ext/securerandom.rb +24 -12
  62. data/lib/active_support/core_ext/string/filters.rb +20 -14
  63. data/lib/active_support/core_ext/string/indent.rb +1 -1
  64. data/lib/active_support/core_ext/string/inflections.rb +16 -5
  65. data/lib/active_support/core_ext/string/output_safety.rb +38 -174
  66. data/lib/active_support/core_ext/thread/backtrace/location.rb +12 -0
  67. data/lib/active_support/core_ext/time/calculations.rb +18 -2
  68. data/lib/active_support/core_ext/time/conversions.rb +2 -2
  69. data/lib/active_support/core_ext/time/zones.rb +4 -4
  70. data/lib/active_support/core_ext/time.rb +0 -1
  71. data/lib/active_support/current_attributes.rb +15 -6
  72. data/lib/active_support/dependencies/autoload.rb +17 -12
  73. data/lib/active_support/deprecation/behaviors.rb +55 -34
  74. data/lib/active_support/deprecation/constant_accessor.rb +5 -4
  75. data/lib/active_support/deprecation/deprecators.rb +104 -0
  76. data/lib/active_support/deprecation/disallowed.rb +3 -5
  77. data/lib/active_support/deprecation/instance_delegator.rb +31 -4
  78. data/lib/active_support/deprecation/method_wrappers.rb +6 -23
  79. data/lib/active_support/deprecation/proxy_wrappers.rb +37 -22
  80. data/lib/active_support/deprecation/reporting.rb +40 -29
  81. data/lib/active_support/deprecation.rb +32 -5
  82. data/lib/active_support/deprecator.rb +7 -0
  83. data/lib/active_support/descendants_tracker.rb +104 -132
  84. data/lib/active_support/duration/iso8601_serializer.rb +0 -2
  85. data/lib/active_support/duration.rb +2 -1
  86. data/lib/active_support/encrypted_configuration.rb +30 -9
  87. data/lib/active_support/encrypted_file.rb +8 -3
  88. data/lib/active_support/environment_inquirer.rb +22 -2
  89. data/lib/active_support/error_reporter/test_helper.rb +15 -0
  90. data/lib/active_support/error_reporter.rb +121 -35
  91. data/lib/active_support/execution_wrapper.rb +4 -4
  92. data/lib/active_support/file_update_checker.rb +4 -2
  93. data/lib/active_support/fork_tracker.rb +10 -2
  94. data/lib/active_support/gem_version.rb +4 -4
  95. data/lib/active_support/gzip.rb +2 -0
  96. data/lib/active_support/hash_with_indifferent_access.rb +35 -17
  97. data/lib/active_support/i18n.rb +1 -1
  98. data/lib/active_support/i18n_railtie.rb +20 -13
  99. data/lib/active_support/inflector/inflections.rb +2 -0
  100. data/lib/active_support/inflector/methods.rb +23 -11
  101. data/lib/active_support/inflector/transliterate.rb +3 -1
  102. data/lib/active_support/isolated_execution_state.rb +26 -22
  103. data/lib/active_support/json/decoding.rb +2 -1
  104. data/lib/active_support/json/encoding.rb +25 -43
  105. data/lib/active_support/key_generator.rb +9 -1
  106. data/lib/active_support/lazy_load_hooks.rb +6 -4
  107. data/lib/active_support/locale/en.yml +2 -0
  108. data/lib/active_support/log_subscriber.rb +78 -33
  109. data/lib/active_support/logger.rb +1 -1
  110. data/lib/active_support/logger_thread_safe_level.rb +9 -22
  111. data/lib/active_support/message_encryptor.rb +197 -53
  112. data/lib/active_support/message_encryptors.rb +140 -0
  113. data/lib/active_support/message_pack/cache_serializer.rb +23 -0
  114. data/lib/active_support/message_pack/extensions.rb +292 -0
  115. data/lib/active_support/message_pack/serializer.rb +63 -0
  116. data/lib/active_support/message_pack.rb +50 -0
  117. data/lib/active_support/message_verifier.rb +212 -93
  118. data/lib/active_support/message_verifiers.rb +134 -0
  119. data/lib/active_support/messages/codec.rb +65 -0
  120. data/lib/active_support/messages/metadata.rb +111 -45
  121. data/lib/active_support/messages/rotation_coordinator.rb +93 -0
  122. data/lib/active_support/messages/rotator.rb +34 -32
  123. data/lib/active_support/messages/serializer_with_fallback.rb +158 -0
  124. data/lib/active_support/multibyte/chars.rb +2 -0
  125. data/lib/active_support/multibyte/unicode.rb +9 -37
  126. data/lib/active_support/notifications/fanout.rb +239 -81
  127. data/lib/active_support/notifications/instrumenter.rb +79 -30
  128. data/lib/active_support/notifications.rb +1 -1
  129. data/lib/active_support/number_helper/number_converter.rb +5 -14
  130. data/lib/active_support/number_helper/number_to_currency_converter.rb +6 -6
  131. data/lib/active_support/number_helper/number_to_human_size_converter.rb +3 -3
  132. data/lib/active_support/number_helper/number_to_phone_converter.rb +1 -0
  133. data/lib/active_support/number_helper.rb +318 -379
  134. data/lib/active_support/ordered_hash.rb +3 -3
  135. data/lib/active_support/ordered_options.rb +14 -0
  136. data/lib/active_support/parameter_filter.rb +84 -69
  137. data/lib/active_support/proxy_object.rb +2 -0
  138. data/lib/active_support/railtie.rb +33 -21
  139. data/lib/active_support/reloader.rb +12 -4
  140. data/lib/active_support/rescuable.rb +2 -0
  141. data/lib/active_support/secure_compare_rotator.rb +16 -9
  142. data/lib/active_support/string_inquirer.rb +3 -1
  143. data/lib/active_support/subscriber.rb +9 -27
  144. data/lib/active_support/syntax_error_proxy.rb +49 -0
  145. data/lib/active_support/tagged_logging.rb +60 -24
  146. data/lib/active_support/test_case.rb +153 -6
  147. data/lib/active_support/testing/assertions.rb +26 -10
  148. data/lib/active_support/testing/autorun.rb +0 -2
  149. data/lib/active_support/testing/constant_stubbing.rb +32 -0
  150. data/lib/active_support/testing/deprecation.rb +25 -25
  151. data/lib/active_support/testing/error_reporter_assertions.rb +108 -0
  152. data/lib/active_support/testing/isolation.rb +1 -1
  153. data/lib/active_support/testing/method_call_assertions.rb +21 -8
  154. data/lib/active_support/testing/parallelize_executor.rb +8 -3
  155. data/lib/active_support/testing/stream.rb +1 -1
  156. data/lib/active_support/testing/strict_warnings.rb +38 -0
  157. data/lib/active_support/testing/time_helpers.rb +32 -14
  158. data/lib/active_support/time_with_zone.rb +4 -14
  159. data/lib/active_support/values/time_zone.rb +9 -7
  160. data/lib/active_support/version.rb +1 -1
  161. data/lib/active_support/xml_mini/jdom.rb +3 -10
  162. data/lib/active_support/xml_mini/nokogiri.rb +1 -1
  163. data/lib/active_support/xml_mini/nokogirisax.rb +1 -1
  164. data/lib/active_support/xml_mini/rexml.rb +1 -1
  165. data/lib/active_support/xml_mini.rb +2 -2
  166. data/lib/active_support.rb +13 -3
  167. metadata +48 -58
  168. data/lib/active_support/core_ext/array/deprecated_conversions.rb +0 -25
  169. data/lib/active_support/core_ext/date/deprecated_conversions.rb +0 -40
  170. data/lib/active_support/core_ext/date_time/deprecated_conversions.rb +0 -36
  171. data/lib/active_support/core_ext/numeric/deprecated_conversions.rb +0 -60
  172. data/lib/active_support/core_ext/range/deprecated_conversions.rb +0 -36
  173. data/lib/active_support/core_ext/range/include_time_with_zone.rb +0 -5
  174. data/lib/active_support/core_ext/range/overlaps.rb +0 -36
  175. data/lib/active_support/core_ext/time/deprecated_conversions.rb +0 -73
  176. data/lib/active_support/core_ext/uri.rb +0 -5
  177. data/lib/active_support/per_thread_registry.rb +0 -65
@@ -9,30 +9,15 @@ rescue LoadError
9
9
  raise
10
10
  end
11
11
 
12
- # Prefer the hiredis driver but don't require it.
13
- begin
14
- if ::Redis::VERSION < "5"
15
- require "redis/connection/hiredis"
16
- else
17
- require "hiredis-client"
18
- end
19
- rescue LoadError
20
- end
21
-
12
+ require "connection_pool"
13
+ require "active_support/core_ext/array/wrap"
14
+ require "active_support/core_ext/hash/slice"
15
+ require "active_support/core_ext/numeric/time"
22
16
  require "active_support/digest"
23
17
 
24
18
  module ActiveSupport
25
19
  module Cache
26
- module ConnectionPoolLike
27
- def with
28
- yield self
29
- end
30
- end
31
-
32
- ::Redis.include(ConnectionPoolLike)
33
- ::Redis::Distributed.include(ConnectionPoolLike)
34
-
35
- # Redis cache store.
20
+ # = Redis \Cache \Store
36
21
  #
37
22
  # Deployment note: Take care to use a *dedicated Redis cache* rather
38
23
  # than pointing this at your existing Redis server. It won't cope well
@@ -53,16 +38,20 @@ module ActiveSupport
53
38
  MAX_KEY_BYTESIZE = 1024
54
39
 
55
40
  DEFAULT_REDIS_OPTIONS = {
56
- connect_timeout: 20,
41
+ connect_timeout: 1,
57
42
  read_timeout: 1,
58
43
  write_timeout: 1,
59
- reconnect_attempts: 0,
60
44
  }
61
45
 
62
46
  DEFAULT_ERROR_HANDLER = -> (method:, returning:, exception:) do
63
47
  if logger
64
48
  logger.error { "RedisCacheStore: #{method} failed, returned #{returning.inspect}: #{exception.class}: #{exception.message}" }
65
49
  end
50
+ ActiveSupport.error_reporter&.report(
51
+ exception,
52
+ severity: :warning,
53
+ source: "redis_cache_store.active_support",
54
+ )
66
55
  end
67
56
 
68
57
  # The maximum number of entries to receive per SCAN call.
@@ -116,8 +105,8 @@ module ActiveSupport
116
105
  end
117
106
  end
118
107
 
119
- attr_reader :redis_options
120
108
  attr_reader :max_key_bytesize
109
+ attr_reader :redis
121
110
 
122
111
  # Creates a new Redis cache store.
123
112
  #
@@ -145,35 +134,31 @@ module ActiveSupport
145
134
  #
146
135
  # Race condition TTL is not set by default. This can be used to avoid
147
136
  # "thundering herd" cache writes when hot cache entries are expired.
148
- # See ActiveSupport::Cache::Store#fetch for more.
149
- def initialize(namespace: nil, compress: true, compress_threshold: 1.kilobyte, coder: default_coder, expires_in: nil, race_condition_ttl: nil, error_handler: DEFAULT_ERROR_HANDLER, **redis_options)
150
- @redis_options = redis_options
137
+ # See <tt>ActiveSupport::Cache::Store#fetch</tt> for more.
138
+ #
139
+ # Setting <tt>skip_nil: true</tt> will not cache nil results:
140
+ #
141
+ # cache.fetch('foo') { nil }
142
+ # cache.fetch('bar', skip_nil: true) { nil }
143
+ # cache.exist?('foo') # => true
144
+ # cache.exist?('bar') # => false
145
+ def initialize(error_handler: DEFAULT_ERROR_HANDLER, **redis_options)
146
+ universal_options = redis_options.extract!(*UNIVERSAL_OPTIONS)
147
+
148
+ if pool_options = self.class.send(:retrieve_pool_options, redis_options)
149
+ @redis = ::ConnectionPool.new(pool_options) { self.class.build_redis(**redis_options) }
150
+ else
151
+ @redis = self.class.build_redis(**redis_options)
152
+ end
151
153
 
152
154
  @max_key_bytesize = MAX_KEY_BYTESIZE
153
155
  @error_handler = error_handler
154
156
 
155
- super namespace: namespace,
156
- compress: compress, compress_threshold: compress_threshold,
157
- expires_in: expires_in, race_condition_ttl: race_condition_ttl,
158
- coder: coder
159
- end
160
-
161
- def redis
162
- @redis ||= begin
163
- pool_options = self.class.send(:retrieve_pool_options, redis_options)
164
-
165
- if pool_options.any?
166
- self.class.send(:ensure_connection_pool_added!)
167
- ::ConnectionPool.new(pool_options) { self.class.build_redis(**redis_options) }
168
- else
169
- self.class.build_redis(**redis_options)
170
- end
171
- end
157
+ super(universal_options)
172
158
  end
173
159
 
174
160
  def inspect
175
- instance = @redis || @redis_options
176
- "#<#{self.class} options=#{options.inspect} redis=#{instance.inspect}>"
161
+ "#<#{self.class} options=#{options.inspect} redis=#{redis.inspect}>"
177
162
  end
178
163
 
179
164
  # Cache Store API implementation.
@@ -181,14 +166,13 @@ module ActiveSupport
181
166
  # Read multiple values at once. Returns a hash of requested keys ->
182
167
  # fetched values.
183
168
  def read_multi(*names)
184
- if mget_capable?
185
- instrument(:read_multi, names, options) do |payload|
186
- read_multi_mget(*names).tap do |results|
187
- payload[:hits] = results.keys
188
- end
169
+ return {} if names.empty?
170
+
171
+ options = names.extract_options!
172
+ instrument_multi(:read_multi, names, options) do |payload|
173
+ read_multi_entries(names, **options).tap do |results|
174
+ payload[:hits] = results.keys
189
175
  end
190
- else
191
- super
192
176
  end
193
177
  end
194
178
 
@@ -212,7 +196,7 @@ module ActiveSupport
212
196
  unless String === matcher
213
197
  raise ArgumentError, "Only Redis glob strings are supported: #{matcher.inspect}"
214
198
  end
215
- redis.with do |c|
199
+ redis.then do |c|
216
200
  pattern = namespace_key(matcher, options)
217
201
  cursor = "0"
218
202
  # Fetch keys in batches using SCAN to avoid blocking the Redis server.
@@ -228,12 +212,21 @@ module ActiveSupport
228
212
  end
229
213
  end
230
214
 
231
- # Cache Store API implementation.
215
+ # Increment a cached integer value using the Redis incrby atomic operator.
216
+ # Returns the updated value.
217
+ #
218
+ # If the key is unset or has expired, it will be set to +amount+:
219
+ #
220
+ # cache.increment("foo") # => 1
221
+ # cache.increment("bar", 100) # => 100
232
222
  #
233
- # Increment a cached value. This method uses the Redis incr atomic
234
- # operator and can only be used on values written with the +:raw+ option.
235
- # Calling it on a value not stored with +:raw+ will initialize that value
236
- # to zero.
223
+ # To set a specific value, call #write passing <tt>raw: true</tt>:
224
+ #
225
+ # cache.write("baz", 5, raw: true)
226
+ # cache.increment("baz") # => 6
227
+ #
228
+ # Incrementing a non-numeric value, or a value written without
229
+ # <tt>raw: true</tt>, will fail and return +nil+.
237
230
  #
238
231
  # Failsafe: Raises errors.
239
232
  def increment(name, amount = 1, options = nil)
@@ -241,22 +234,25 @@ module ActiveSupport
241
234
  failsafe :increment do
242
235
  options = merged_options(options)
243
236
  key = normalize_key(name, options)
244
-
245
- redis.with do |c|
246
- c.incrby(key, amount).tap do
247
- write_key_expiry(c, key, options)
248
- end
249
- end
237
+ change_counter(key, amount, options)
250
238
  end
251
239
  end
252
240
  end
253
241
 
254
- # Cache Store API implementation.
242
+ # Decrement a cached integer value using the Redis decrby atomic operator.
243
+ # Returns the updated value.
244
+ #
245
+ # If the key is unset or has expired, it will be set to -amount:
246
+ #
247
+ # cache.decrement("foo") # => -1
255
248
  #
256
- # Decrement a cached value. This method uses the Redis decr atomic
257
- # operator and can only be used on values written with the +:raw+ option.
258
- # Calling it on a value not stored with +:raw+ will initialize that value
259
- # to zero.
249
+ # To set a specific value, call #write passing <tt>raw: true</tt>:
250
+ #
251
+ # cache.write("baz", 5, raw: true)
252
+ # cache.decrement("baz") # => 4
253
+ #
254
+ # Decrementing a non-numeric value, or a value written without
255
+ # <tt>raw: true</tt>, will fail and return +nil+.
260
256
  #
261
257
  # Failsafe: Raises errors.
262
258
  def decrement(name, amount = 1, options = nil)
@@ -264,12 +260,7 @@ module ActiveSupport
264
260
  failsafe :decrement do
265
261
  options = merged_options(options)
266
262
  key = normalize_key(name, options)
267
-
268
- redis.with do |c|
269
- c.decrby(key, amount).tap do
270
- write_key_expiry(c, key, options)
271
- end
272
- end
263
+ change_counter(key, -amount, options)
273
264
  end
274
265
  end
275
266
  end
@@ -291,38 +282,27 @@ module ActiveSupport
291
282
  if namespace = merged_options(options)[:namespace]
292
283
  delete_matched "*", namespace: namespace
293
284
  else
294
- redis.with { |c| c.flushdb }
285
+ redis.then { |c| c.flushdb }
295
286
  end
296
287
  end
297
288
  end
298
289
 
299
290
  # Get info from redis servers.
300
291
  def stats
301
- redis.with { |c| c.info }
302
- end
303
-
304
- def mget_capable? # :nodoc:
305
- set_redis_capabilities unless defined? @mget_capable
306
- @mget_capable
307
- end
308
-
309
- def mset_capable? # :nodoc:
310
- set_redis_capabilities unless defined? @mset_capable
311
- @mset_capable
292
+ redis.then { |c| c.info }
312
293
  end
313
294
 
314
295
  private
315
- def set_redis_capabilities
316
- redis.with do |c|
317
- case c
318
- when Redis::Distributed
319
- @mget_capable = true
320
- @mset_capable = false
296
+ def pipeline_entries(entries, &block)
297
+ redis.then { |c|
298
+ if c.is_a?(Redis::Distributed)
299
+ entries.group_by { |k, _v| c.node_for(k) }.each do |node, sub_entries|
300
+ node.pipelined { |pipe| yield(pipe, sub_entries) }
301
+ end
321
302
  else
322
- @mget_capable = true
323
- @mset_capable = true
303
+ c.pipelined { |pipe| yield(pipe, entries) }
324
304
  end
325
- end
305
+ }
326
306
  end
327
307
 
328
308
  # Store provider interface:
@@ -333,28 +313,19 @@ module ActiveSupport
333
313
 
334
314
  def read_serialized_entry(key, raw: false, **options)
335
315
  failsafe :read_entry do
336
- redis.with { |c| c.get(key) }
316
+ redis.then { |c| c.get(key) }
337
317
  end
338
318
  end
339
319
 
340
320
  def read_multi_entries(names, **options)
341
- if mget_capable?
342
- read_multi_mget(*names, **options)
343
- else
344
- super
345
- end
346
- end
347
-
348
- def read_multi_mget(*names)
349
- options = names.extract_options!
350
321
  options = merged_options(options)
351
322
  return {} if names == []
352
323
  raw = options&.fetch(:raw, false)
353
324
 
354
325
  keys = names.map { |name| normalize_key(name, options) }
355
326
 
356
- values = failsafe(:read_multi_mget, returning: {}) do
357
- redis.with { |c| c.mget(*keys) }
327
+ values = failsafe(:read_multi_entries, returning: {}) do
328
+ redis.then { |c| c.mget(*keys) }
358
329
  end
359
330
 
360
331
  names.zip(values).each_with_object({}) do |(name, value), results|
@@ -374,7 +345,7 @@ module ActiveSupport
374
345
  write_serialized_entry(key, serialize_entry(entry, raw: raw, **options), raw: raw, **options)
375
346
  end
376
347
 
377
- def write_serialized_entry(key, payload, raw: false, unless_exist: false, expires_in: nil, race_condition_ttl: nil, **options)
348
+ def write_serialized_entry(key, payload, raw: false, unless_exist: false, expires_in: nil, race_condition_ttl: nil, pipeline: nil, **options)
378
349
  # If race condition TTL is in use, ensure that cache entries
379
350
  # stick around a bit longer after they would have expired
380
351
  # so we can purposefully serve stale entries.
@@ -388,41 +359,40 @@ module ActiveSupport
388
359
  modifiers[:px] = (1000 * expires_in.to_f).ceil if expires_in
389
360
  end
390
361
 
391
- failsafe :write_entry, returning: false do
392
- redis.with { |c| c.set key, payload, **modifiers }
393
- end
394
- end
395
-
396
- def write_key_expiry(client, key, options)
397
- if options[:expires_in] && client.ttl(key).negative?
398
- client.expire key, options[:expires_in].to_i
362
+ if pipeline
363
+ pipeline.set(key, payload, **modifiers)
364
+ else
365
+ failsafe :write_entry, returning: false do
366
+ redis.then { |c| c.set key, payload, **modifiers }
367
+ end
399
368
  end
400
369
  end
401
370
 
402
371
  # Delete an entry from the cache.
403
- def delete_entry(key, options)
372
+ def delete_entry(key, **options)
404
373
  failsafe :delete_entry, returning: false do
405
- redis.with { |c| c.del key }
374
+ redis.then { |c| c.del(key) == 1 }
406
375
  end
407
376
  end
408
377
 
409
378
  # Deletes multiple entries in the cache. Returns the number of entries deleted.
410
379
  def delete_multi_entries(entries, **_options)
411
- redis.with { |c| c.del(entries) }
380
+ failsafe :delete_multi_entries, returning: 0 do
381
+ redis.then { |c| c.del(entries) }
382
+ end
412
383
  end
413
384
 
414
385
  # Nonstandard store provider API to write multiple values at once.
415
- def write_multi_entries(entries, expires_in: nil, **options)
416
- if entries.any?
417
- if mset_capable? && expires_in.nil?
418
- failsafe :write_multi_entries do
419
- payload = serialize_entries(entries, **options)
420
- redis.with do |c|
421
- c.mapped_mset(payload)
422
- end
386
+ def write_multi_entries(entries, expires_in: nil, race_condition_ttl: nil, **options)
387
+ return if entries.empty?
388
+
389
+ failsafe :write_multi_entries do
390
+ pipeline_entries(entries) do |pipeline, sharded_entries|
391
+ options = options.dup
392
+ options[:pipeline] = pipeline
393
+ sharded_entries.each do |key, entry|
394
+ write_entry key, entry, **options
423
395
  end
424
- else
425
- super
426
396
  end
427
397
  end
428
398
  end
@@ -464,10 +434,35 @@ module ActiveSupport
464
434
  end
465
435
  end
466
436
 
437
+ def change_counter(key, amount, options)
438
+ redis.then do |c|
439
+ c = c.node_for(key) if c.is_a?(Redis::Distributed)
440
+
441
+ if options[:expires_in] && supports_expire_nx?
442
+ c.pipelined do |pipeline|
443
+ pipeline.incrby(key, amount)
444
+ pipeline.call(:expire, key, options[:expires_in].to_i, "NX")
445
+ end.first
446
+ else
447
+ count = c.incrby(key, amount)
448
+ if count != amount && options[:expires_in] && c.ttl(key) < 0
449
+ c.expire(key, options[:expires_in].to_i)
450
+ end
451
+ count
452
+ end
453
+ end
454
+ end
455
+
456
+ def supports_expire_nx?
457
+ return @supports_expire_nx if defined?(@supports_expire_nx)
458
+
459
+ redis_versions = redis.then { |c| Array.wrap(c.info("server")).pluck("redis_version") }
460
+ @supports_expire_nx = redis_versions.all? { |v| Gem::Version.new(v) >= Gem::Version.new("7.0.0") }
461
+ end
462
+
467
463
  def failsafe(method, returning: nil)
468
464
  yield
469
465
  rescue ::Redis::BaseError => error
470
- ActiveSupport.error_reporter&.report(error, handled: true, severity: :warning)
471
466
  @error_handler&.call(method: method, exception: error, returning: returning)
472
467
  returning
473
468
  end
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "zlib"
4
+ require "active_support/core_ext/kernel/reporting"
5
+
6
+ module ActiveSupport
7
+ module Cache
8
+ module SerializerWithFallback # :nodoc:
9
+ def self.[](format)
10
+ if format.to_s.include?("message_pack") && !defined?(ActiveSupport::MessagePack)
11
+ require "active_support/message_pack"
12
+ end
13
+
14
+ SERIALIZERS.fetch(format)
15
+ end
16
+
17
+ def load(dumped)
18
+ if dumped.is_a?(String)
19
+ case
20
+ when MessagePackWithFallback.dumped?(dumped)
21
+ MessagePackWithFallback._load(dumped)
22
+ when Marshal71WithFallback.dumped?(dumped)
23
+ Marshal71WithFallback._load(dumped)
24
+ when Marshal70WithFallback.dumped?(dumped)
25
+ Marshal70WithFallback._load(dumped)
26
+ else
27
+ Cache::Store.logger&.warn("Unrecognized payload prefix #{dumped.byteslice(0).inspect}; deserializing as nil")
28
+ nil
29
+ end
30
+ elsif PassthroughWithFallback.dumped?(dumped)
31
+ PassthroughWithFallback._load(dumped)
32
+ else
33
+ Cache::Store.logger&.warn("Unrecognized payload class #{dumped.class}; deserializing as nil")
34
+ nil
35
+ end
36
+ end
37
+
38
+ private
39
+ def marshal_load(payload)
40
+ Marshal.load(payload)
41
+ rescue ArgumentError => error
42
+ raise Cache::DeserializationError, error.message
43
+ end
44
+
45
+ module PassthroughWithFallback
46
+ include SerializerWithFallback
47
+ extend self
48
+
49
+ def dump(entry)
50
+ entry
51
+ end
52
+
53
+ def dump_compressed(entry, threshold)
54
+ entry.compressed(threshold)
55
+ end
56
+
57
+ def _load(entry)
58
+ entry
59
+ end
60
+
61
+ def dumped?(dumped)
62
+ dumped.is_a?(Cache::Entry)
63
+ end
64
+ end
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
+ module Marshal70WithFallback
89
+ include SerializerWithFallback
90
+ extend self
91
+
92
+ MARK_UNCOMPRESSED = "\x00".b.freeze
93
+ MARK_COMPRESSED = "\x01".b.freeze
94
+
95
+ def dump(entry)
96
+ MARK_UNCOMPRESSED + Marshal.dump(entry.pack)
97
+ end
98
+
99
+ def dump_compressed(entry, threshold)
100
+ dumped = Marshal.dump(entry.pack)
101
+
102
+ if dumped.bytesize >= threshold
103
+ compressed = Zlib::Deflate.deflate(dumped)
104
+ return MARK_COMPRESSED + compressed if compressed.bytesize < dumped.bytesize
105
+ end
106
+
107
+ MARK_UNCOMPRESSED + dumped
108
+ end
109
+
110
+ def _load(marked)
111
+ dumped = marked.byteslice(1..-1)
112
+ dumped = Zlib::Inflate.inflate(dumped) if marked.start_with?(MARK_COMPRESSED)
113
+ Cache::Entry.unpack(marshal_load(dumped))
114
+ end
115
+
116
+ def dumped?(dumped)
117
+ dumped.start_with?(MARK_UNCOMPRESSED, MARK_COMPRESSED)
118
+ end
119
+ end
120
+
121
+ module Marshal71WithFallback
122
+ include SerializerWithFallback
123
+ extend self
124
+
125
+ MARSHAL_SIGNATURE = "\x04\x08".b.freeze
126
+
127
+ def dump(value)
128
+ Marshal.dump(value)
129
+ end
130
+
131
+ def _load(dumped)
132
+ marshal_load(dumped)
133
+ end
134
+
135
+ def dumped?(dumped)
136
+ dumped.start_with?(MARSHAL_SIGNATURE)
137
+ end
138
+ end
139
+
140
+ module MessagePackWithFallback
141
+ include SerializerWithFallback
142
+ extend self
143
+
144
+ def dump(value)
145
+ ActiveSupport::MessagePack::CacheSerializer.dump(value)
146
+ end
147
+
148
+ def _load(dumped)
149
+ ActiveSupport::MessagePack::CacheSerializer.load(dumped)
150
+ end
151
+
152
+ def dumped?(dumped)
153
+ available? && ActiveSupport::MessagePack.signature?(dumped)
154
+ end
155
+
156
+ private
157
+ def available?
158
+ return @available if defined?(@available)
159
+ silence_warnings { require "active_support/message_pack" }
160
+ @available = true
161
+ rescue LoadError
162
+ @available = false
163
+ end
164
+ end
165
+
166
+ SERIALIZERS = {
167
+ passthrough: PassthroughWithFallback,
168
+ marshal_6_1: Marshal61WithFallback,
169
+ marshal_7_0: Marshal70WithFallback,
170
+ marshal_7_1: Marshal71WithFallback,
171
+ message_pack: MessagePackWithFallback,
172
+ }
173
+ end
174
+ end
175
+ end
@@ -5,6 +5,8 @@ require "active_support/core_ext/string/inflections"
5
5
  module ActiveSupport
6
6
  module Cache
7
7
  module Strategy
8
+ # = Local \Cache \Strategy
9
+ #
8
10
  # Caches that implement LocalCache will be backed by an in-memory cache for the
9
11
  # duration of a block. Repeated calls to the cache for the same key will hit the
10
12
  # in-memory cache for faster access.
@@ -26,6 +28,8 @@ module ActiveSupport
26
28
  end
27
29
  end
28
30
 
31
+ # = Local \Cache \Store
32
+ #
29
33
  # Simple memory backed cache. This cache is not thread safe and is intended only
30
34
  # for serving as a temporary memory cache for a single thread.
31
35
  class LocalStore
@@ -72,35 +76,43 @@ module ActiveSupport
72
76
  local_cache_key)
73
77
  end
74
78
 
75
- def clear(**options) # :nodoc:
79
+ def clear(options = nil) # :nodoc:
76
80
  return super unless cache = local_cache
77
81
  cache.clear(options)
78
82
  super
79
83
  end
80
84
 
81
- def cleanup(**options) # :nodoc:
85
+ def cleanup(options = nil) # :nodoc:
82
86
  return super unless cache = local_cache
83
- cache.clear
87
+ cache.clear(options)
84
88
  super
85
89
  end
86
90
 
87
91
  def delete_matched(matcher, options = nil) # :nodoc:
88
92
  return super unless cache = local_cache
89
- cache.clear
93
+ cache.clear(options)
90
94
  super
91
95
  end
92
96
 
93
- def increment(name, amount = 1, **options) # :nodoc:
97
+ def increment(name, amount = 1, options = nil) # :nodoc:
94
98
  return super unless local_cache
95
99
  value = bypass_local_cache { super }
96
- write_cache_value(name, value, raw: true, **options)
100
+ if options
101
+ write_cache_value(name, value, raw: true, **options)
102
+ else
103
+ write_cache_value(name, value, raw: true)
104
+ end
97
105
  value
98
106
  end
99
107
 
100
- def decrement(name, amount = 1, **options) # :nodoc:
108
+ def decrement(name, amount = 1, options = nil) # :nodoc:
101
109
  return super unless local_cache
102
110
  value = bypass_local_cache { super }
103
- write_cache_value(name, value, raw: true, **options)
111
+ if options
112
+ write_cache_value(name, value, raw: true, **options)
113
+ else
114
+ write_cache_value(name, value, raw: true)
115
+ end
104
116
  value
105
117
  end
106
118