activesupport 7.0.8.1 → 7.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (175) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +818 -293
  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/broadcast_logger.rb +242 -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 +128 -0
  13. data/lib/active_support/cache/file_store.rb +36 -9
  14. data/lib/active_support/cache/mem_cache_store.rb +84 -68
  15. data/lib/active_support/cache/memory_store.rb +75 -21
  16. data/lib/active_support/cache/null_store.rb +6 -0
  17. data/lib/active_support/cache/redis_cache_store.rb +134 -131
  18. data/lib/active_support/cache/serializer_with_fallback.rb +175 -0
  19. data/lib/active_support/cache/strategy/local_cache.rb +20 -8
  20. data/lib/active_support/cache.rb +298 -246
  21. data/lib/active_support/callbacks.rb +44 -21
  22. data/lib/active_support/concern.rb +4 -2
  23. data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +42 -3
  24. data/lib/active_support/concurrency/null_lock.rb +13 -0
  25. data/lib/active_support/configurable.rb +10 -0
  26. data/lib/active_support/core_ext/array/conversions.rb +2 -1
  27. data/lib/active_support/core_ext/array.rb +0 -1
  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/hash/deep_merge.rb +22 -14
  39. data/lib/active_support/core_ext/module/attribute_accessors.rb +6 -0
  40. data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +34 -16
  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/inclusion.rb +13 -5
  49. data/lib/active_support/core_ext/object/instance_variables.rb +22 -12
  50. data/lib/active_support/core_ext/object/json.rb +11 -3
  51. data/lib/active_support/core_ext/object/with.rb +44 -0
  52. data/lib/active_support/core_ext/object/with_options.rb +3 -3
  53. data/lib/active_support/core_ext/object.rb +1 -0
  54. data/lib/active_support/core_ext/pathname/blank.rb +16 -0
  55. data/lib/active_support/core_ext/pathname/existence.rb +2 -0
  56. data/lib/active_support/core_ext/pathname.rb +1 -0
  57. data/lib/active_support/core_ext/range/conversions.rb +28 -7
  58. data/lib/active_support/core_ext/range/overlap.rb +40 -0
  59. data/lib/active_support/core_ext/range.rb +1 -2
  60. data/lib/active_support/core_ext/securerandom.rb +24 -12
  61. data/lib/active_support/core_ext/string/filters.rb +20 -14
  62. data/lib/active_support/core_ext/string/inflections.rb +16 -5
  63. data/lib/active_support/core_ext/string/output_safety.rb +38 -174
  64. data/lib/active_support/core_ext/thread/backtrace/location.rb +12 -0
  65. data/lib/active_support/core_ext/time/calculations.rb +18 -2
  66. data/lib/active_support/core_ext/time/conversions.rb +2 -2
  67. data/lib/active_support/core_ext/time/zones.rb +4 -4
  68. data/lib/active_support/core_ext/time.rb +0 -1
  69. data/lib/active_support/current_attributes.rb +15 -6
  70. data/lib/active_support/deep_mergeable.rb +53 -0
  71. data/lib/active_support/dependencies/autoload.rb +17 -12
  72. data/lib/active_support/deprecation/behaviors.rb +55 -34
  73. data/lib/active_support/deprecation/constant_accessor.rb +5 -4
  74. data/lib/active_support/deprecation/deprecators.rb +104 -0
  75. data/lib/active_support/deprecation/disallowed.rb +3 -5
  76. data/lib/active_support/deprecation/instance_delegator.rb +31 -4
  77. data/lib/active_support/deprecation/method_wrappers.rb +6 -23
  78. data/lib/active_support/deprecation/proxy_wrappers.rb +37 -22
  79. data/lib/active_support/deprecation/reporting.rb +35 -21
  80. data/lib/active_support/deprecation.rb +32 -5
  81. data/lib/active_support/deprecator.rb +7 -0
  82. data/lib/active_support/descendants_tracker.rb +104 -132
  83. data/lib/active_support/duration/iso8601_serializer.rb +0 -2
  84. data/lib/active_support/duration.rb +2 -1
  85. data/lib/active_support/encrypted_configuration.rb +30 -9
  86. data/lib/active_support/encrypted_file.rb +8 -3
  87. data/lib/active_support/environment_inquirer.rb +22 -2
  88. data/lib/active_support/error_reporter/test_helper.rb +15 -0
  89. data/lib/active_support/error_reporter.rb +121 -35
  90. data/lib/active_support/execution_wrapper.rb +4 -4
  91. data/lib/active_support/file_update_checker.rb +4 -2
  92. data/lib/active_support/fork_tracker.rb +10 -2
  93. data/lib/active_support/gem_version.rb +4 -4
  94. data/lib/active_support/gzip.rb +2 -0
  95. data/lib/active_support/hash_with_indifferent_access.rb +35 -17
  96. data/lib/active_support/i18n.rb +1 -1
  97. data/lib/active_support/i18n_railtie.rb +20 -13
  98. data/lib/active_support/inflector/inflections.rb +2 -0
  99. data/lib/active_support/inflector/methods.rb +22 -10
  100. data/lib/active_support/inflector/transliterate.rb +3 -1
  101. data/lib/active_support/isolated_execution_state.rb +26 -22
  102. data/lib/active_support/json/decoding.rb +2 -1
  103. data/lib/active_support/json/encoding.rb +25 -43
  104. data/lib/active_support/key_generator.rb +9 -1
  105. data/lib/active_support/lazy_load_hooks.rb +6 -4
  106. data/lib/active_support/locale/en.yml +2 -0
  107. data/lib/active_support/log_subscriber.rb +78 -33
  108. data/lib/active_support/logger.rb +9 -60
  109. data/lib/active_support/logger_thread_safe_level.rb +10 -24
  110. data/lib/active_support/message_encryptor.rb +197 -53
  111. data/lib/active_support/message_encryptors.rb +141 -0
  112. data/lib/active_support/message_pack/cache_serializer.rb +23 -0
  113. data/lib/active_support/message_pack/extensions.rb +292 -0
  114. data/lib/active_support/message_pack/serializer.rb +63 -0
  115. data/lib/active_support/message_pack.rb +50 -0
  116. data/lib/active_support/message_verifier.rb +212 -93
  117. data/lib/active_support/message_verifiers.rb +135 -0
  118. data/lib/active_support/messages/codec.rb +65 -0
  119. data/lib/active_support/messages/metadata.rb +111 -45
  120. data/lib/active_support/messages/rotation_coordinator.rb +93 -0
  121. data/lib/active_support/messages/rotator.rb +34 -32
  122. data/lib/active_support/messages/serializer_with_fallback.rb +158 -0
  123. data/lib/active_support/multibyte/chars.rb +2 -0
  124. data/lib/active_support/multibyte/unicode.rb +9 -37
  125. data/lib/active_support/notifications/fanout.rb +239 -81
  126. data/lib/active_support/notifications/instrumenter.rb +77 -20
  127. data/lib/active_support/notifications.rb +1 -1
  128. data/lib/active_support/number_helper/number_converter.rb +14 -5
  129. data/lib/active_support/number_helper/number_to_currency_converter.rb +6 -6
  130. data/lib/active_support/number_helper/number_to_human_size_converter.rb +1 -1
  131. data/lib/active_support/number_helper/number_to_phone_converter.rb +1 -0
  132. data/lib/active_support/ordered_hash.rb +3 -3
  133. data/lib/active_support/ordered_options.rb +14 -0
  134. data/lib/active_support/parameter_filter.rb +84 -69
  135. data/lib/active_support/proxy_object.rb +2 -0
  136. data/lib/active_support/railtie.rb +33 -21
  137. data/lib/active_support/reloader.rb +12 -4
  138. data/lib/active_support/rescuable.rb +2 -0
  139. data/lib/active_support/secure_compare_rotator.rb +16 -9
  140. data/lib/active_support/string_inquirer.rb +3 -1
  141. data/lib/active_support/subscriber.rb +9 -27
  142. data/lib/active_support/syntax_error_proxy.rb +49 -0
  143. data/lib/active_support/tagged_logging.rb +60 -24
  144. data/lib/active_support/test_case.rb +153 -6
  145. data/lib/active_support/testing/assertions.rb +25 -9
  146. data/lib/active_support/testing/autorun.rb +0 -2
  147. data/lib/active_support/testing/constant_stubbing.rb +32 -0
  148. data/lib/active_support/testing/deprecation.rb +25 -25
  149. data/lib/active_support/testing/error_reporter_assertions.rb +107 -0
  150. data/lib/active_support/testing/isolation.rb +1 -1
  151. data/lib/active_support/testing/method_call_assertions.rb +21 -8
  152. data/lib/active_support/testing/parallelize_executor.rb +8 -3
  153. data/lib/active_support/testing/stream.rb +1 -1
  154. data/lib/active_support/testing/strict_warnings.rb +38 -0
  155. data/lib/active_support/testing/time_helpers.rb +32 -14
  156. data/lib/active_support/time_with_zone.rb +4 -14
  157. data/lib/active_support/values/time_zone.rb +9 -7
  158. data/lib/active_support/version.rb +1 -1
  159. data/lib/active_support/xml_mini/jdom.rb +3 -10
  160. data/lib/active_support/xml_mini/nokogiri.rb +1 -1
  161. data/lib/active_support/xml_mini/nokogirisax.rb +1 -1
  162. data/lib/active_support/xml_mini/rexml.rb +1 -1
  163. data/lib/active_support/xml_mini.rb +2 -2
  164. data/lib/active_support.rb +14 -3
  165. metadata +103 -16
  166. data/lib/active_support/core_ext/array/deprecated_conversions.rb +0 -25
  167. data/lib/active_support/core_ext/date/deprecated_conversions.rb +0 -40
  168. data/lib/active_support/core_ext/date_time/deprecated_conversions.rb +0 -36
  169. data/lib/active_support/core_ext/numeric/deprecated_conversions.rb +0 -60
  170. data/lib/active_support/core_ext/range/deprecated_conversions.rb +0 -36
  171. data/lib/active_support/core_ext/range/include_time_with_zone.rb +0 -5
  172. data/lib/active_support/core_ext/range/overlaps.rb +0 -10
  173. data/lib/active_support/core_ext/time/deprecated_conversions.rb +0 -73
  174. data/lib/active_support/core_ext/uri.rb +0 -5
  175. 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
222
+ #
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
232
227
  #
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.
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
248
+ #
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
255
253
  #
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.
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,43 @@ 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
+ expires_in = options[:expires_in]
442
+
443
+ if expires_in
444
+ if supports_expire_nx?
445
+ count, _ = c.pipelined do |pipeline|
446
+ pipeline.incrby(key, amount)
447
+ pipeline.call(:expire, key, expires_in.to_i, "NX")
448
+ end
449
+ else
450
+ count, ttl = c.pipelined do |pipeline|
451
+ pipeline.incrby(key, amount)
452
+ pipeline.ttl(key)
453
+ end
454
+ c.expire(key, expires_in.to_i) if ttl < 0
455
+ end
456
+ else
457
+ count = c.incrby(key, amount)
458
+ end
459
+
460
+ count
461
+ end
462
+ end
463
+
464
+ def supports_expire_nx?
465
+ return @supports_expire_nx if defined?(@supports_expire_nx)
466
+
467
+ redis_versions = redis.then { |c| Array.wrap(c.info("server")).pluck("redis_version") }
468
+ @supports_expire_nx = redis_versions.all? { |v| Gem::Version.new(v) >= Gem::Version.new("7.0.0") }
469
+ end
470
+
467
471
  def failsafe(method, returning: nil)
468
472
  yield
469
473
  rescue ::Redis::BaseError => error
470
- ActiveSupport.error_reporter&.report(error, handled: true, severity: :warning)
471
474
  @error_handler&.call(method: method, exception: error, returning: returning)
472
475
  returning
473
476
  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