activesupport 7.0.8 → 7.1.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (178) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +921 -287
  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 +250 -0
  10. data/lib/active_support/builder.rb +1 -1
  11. data/lib/active_support/cache/coder.rb +153 -0
  12. data/lib/active_support/cache/entry.rb +134 -0
  13. data/lib/active_support/cache/file_store.rb +37 -10
  14. data/lib/active_support/cache/mem_cache_store.rb +100 -76
  15. data/lib/active_support/cache/memory_store.rb +78 -24
  16. data/lib/active_support/cache/null_store.rb +6 -0
  17. data/lib/active_support/cache/redis_cache_store.rb +151 -141
  18. data/lib/active_support/cache/serializer_with_fallback.rb +175 -0
  19. data/lib/active_support/cache/strategy/local_cache.rb +29 -14
  20. data/lib/active_support/cache.rb +331 -252
  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 +2 -1
  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/concerning.rb +6 -6
  42. data/lib/active_support/core_ext/module/delegation.rb +40 -11
  43. data/lib/active_support/core_ext/module/deprecation.rb +15 -12
  44. data/lib/active_support/core_ext/module/introspection.rb +0 -1
  45. data/lib/active_support/core_ext/numeric/bytes.rb +9 -0
  46. data/lib/active_support/core_ext/numeric/conversions.rb +2 -0
  47. data/lib/active_support/core_ext/numeric.rb +0 -1
  48. data/lib/active_support/core_ext/object/deep_dup.rb +16 -0
  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 +11 -3
  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 +40 -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/deep_mergeable.rb +53 -0
  73. data/lib/active_support/dependencies/autoload.rb +17 -12
  74. data/lib/active_support/deprecation/behaviors.rb +65 -42
  75. data/lib/active_support/deprecation/constant_accessor.rb +5 -4
  76. data/lib/active_support/deprecation/deprecators.rb +104 -0
  77. data/lib/active_support/deprecation/disallowed.rb +3 -5
  78. data/lib/active_support/deprecation/instance_delegator.rb +31 -4
  79. data/lib/active_support/deprecation/method_wrappers.rb +6 -23
  80. data/lib/active_support/deprecation/proxy_wrappers.rb +37 -22
  81. data/lib/active_support/deprecation/reporting.rb +42 -25
  82. data/lib/active_support/deprecation.rb +32 -5
  83. data/lib/active_support/deprecator.rb +7 -0
  84. data/lib/active_support/descendants_tracker.rb +104 -132
  85. data/lib/active_support/duration/iso8601_serializer.rb +0 -2
  86. data/lib/active_support/duration.rb +2 -1
  87. data/lib/active_support/encrypted_configuration.rb +30 -9
  88. data/lib/active_support/encrypted_file.rb +8 -3
  89. data/lib/active_support/environment_inquirer.rb +22 -2
  90. data/lib/active_support/error_reporter/test_helper.rb +15 -0
  91. data/lib/active_support/error_reporter.rb +121 -35
  92. data/lib/active_support/execution_wrapper.rb +4 -4
  93. data/lib/active_support/file_update_checker.rb +4 -2
  94. data/lib/active_support/fork_tracker.rb +10 -2
  95. data/lib/active_support/gem_version.rb +3 -3
  96. data/lib/active_support/gzip.rb +2 -0
  97. data/lib/active_support/hash_with_indifferent_access.rb +35 -17
  98. data/lib/active_support/i18n.rb +1 -1
  99. data/lib/active_support/i18n_railtie.rb +20 -13
  100. data/lib/active_support/inflector/inflections.rb +2 -0
  101. data/lib/active_support/inflector/methods.rb +23 -11
  102. data/lib/active_support/inflector/transliterate.rb +3 -1
  103. data/lib/active_support/isolated_execution_state.rb +26 -22
  104. data/lib/active_support/json/decoding.rb +2 -1
  105. data/lib/active_support/json/encoding.rb +25 -43
  106. data/lib/active_support/key_generator.rb +9 -1
  107. data/lib/active_support/lazy_load_hooks.rb +6 -4
  108. data/lib/active_support/locale/en.yml +2 -0
  109. data/lib/active_support/log_subscriber.rb +84 -33
  110. data/lib/active_support/logger.rb +9 -60
  111. data/lib/active_support/logger_thread_safe_level.rb +10 -24
  112. data/lib/active_support/message_encryptor.rb +197 -53
  113. data/lib/active_support/message_encryptors.rb +141 -0
  114. data/lib/active_support/message_pack/cache_serializer.rb +23 -0
  115. data/lib/active_support/message_pack/extensions.rb +292 -0
  116. data/lib/active_support/message_pack/serializer.rb +63 -0
  117. data/lib/active_support/message_pack.rb +50 -0
  118. data/lib/active_support/message_verifier.rb +212 -93
  119. data/lib/active_support/message_verifiers.rb +135 -0
  120. data/lib/active_support/messages/codec.rb +65 -0
  121. data/lib/active_support/messages/metadata.rb +111 -45
  122. data/lib/active_support/messages/rotation_coordinator.rb +93 -0
  123. data/lib/active_support/messages/rotator.rb +34 -32
  124. data/lib/active_support/messages/serializer_with_fallback.rb +158 -0
  125. data/lib/active_support/multibyte/chars.rb +2 -0
  126. data/lib/active_support/multibyte/unicode.rb +9 -37
  127. data/lib/active_support/notifications/fanout.rb +245 -81
  128. data/lib/active_support/notifications/instrumenter.rb +77 -20
  129. data/lib/active_support/notifications.rb +1 -1
  130. data/lib/active_support/number_helper/number_converter.rb +14 -5
  131. data/lib/active_support/number_helper/number_to_currency_converter.rb +6 -6
  132. data/lib/active_support/number_helper/number_to_human_size_converter.rb +3 -3
  133. data/lib/active_support/number_helper/number_to_phone_converter.rb +1 -0
  134. data/lib/active_support/number_helper.rb +379 -318
  135. data/lib/active_support/ordered_hash.rb +3 -3
  136. data/lib/active_support/ordered_options.rb +14 -0
  137. data/lib/active_support/parameter_filter.rb +84 -69
  138. data/lib/active_support/proxy_object.rb +2 -0
  139. data/lib/active_support/railtie.rb +33 -21
  140. data/lib/active_support/reloader.rb +12 -4
  141. data/lib/active_support/rescuable.rb +2 -0
  142. data/lib/active_support/secure_compare_rotator.rb +16 -9
  143. data/lib/active_support/string_inquirer.rb +3 -1
  144. data/lib/active_support/subscriber.rb +9 -27
  145. data/lib/active_support/syntax_error_proxy.rb +70 -0
  146. data/lib/active_support/tagged_logging.rb +60 -24
  147. data/lib/active_support/test_case.rb +153 -6
  148. data/lib/active_support/testing/assertions.rb +26 -10
  149. data/lib/active_support/testing/autorun.rb +0 -2
  150. data/lib/active_support/testing/constant_stubbing.rb +32 -0
  151. data/lib/active_support/testing/deprecation.rb +25 -25
  152. data/lib/active_support/testing/error_reporter_assertions.rb +107 -0
  153. data/lib/active_support/testing/isolation.rb +1 -1
  154. data/lib/active_support/testing/method_call_assertions.rb +21 -8
  155. data/lib/active_support/testing/parallelize_executor.rb +8 -3
  156. data/lib/active_support/testing/stream.rb +1 -1
  157. data/lib/active_support/testing/strict_warnings.rb +39 -0
  158. data/lib/active_support/testing/time_helpers.rb +37 -15
  159. data/lib/active_support/time_with_zone.rb +4 -14
  160. data/lib/active_support/values/time_zone.rb +9 -7
  161. data/lib/active_support/version.rb +1 -1
  162. data/lib/active_support/xml_mini/jdom.rb +3 -10
  163. data/lib/active_support/xml_mini/nokogiri.rb +1 -1
  164. data/lib/active_support/xml_mini/nokogirisax.rb +1 -1
  165. data/lib/active_support/xml_mini/rexml.rb +1 -1
  166. data/lib/active_support/xml_mini.rb +2 -2
  167. data/lib/active_support.rb +14 -3
  168. metadata +102 -15
  169. data/lib/active_support/core_ext/array/deprecated_conversions.rb +0 -25
  170. data/lib/active_support/core_ext/date/deprecated_conversions.rb +0 -40
  171. data/lib/active_support/core_ext/date_time/deprecated_conversions.rb +0 -36
  172. data/lib/active_support/core_ext/numeric/deprecated_conversions.rb +0 -60
  173. data/lib/active_support/core_ext/range/deprecated_conversions.rb +0 -36
  174. data/lib/active_support/core_ext/range/include_time_with_zone.rb +0 -5
  175. data/lib/active_support/core_ext/range/overlaps.rb +0 -10
  176. data/lib/active_support/core_ext/time/deprecated_conversions.rb +0 -73
  177. data/lib/active_support/core_ext/uri.rb +0 -5
  178. data/lib/active_support/per_thread_registry.rb +0 -65
@@ -9,60 +9,50 @@ 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
- # Deployment note: Take care to use a *dedicated Redis cache* rather
38
- # than pointing this at your existing Redis server. It won't cope well
39
- # with mixed usage patterns and it won't expire cache entries by default.
22
+ # Deployment note: Take care to use a <b>dedicated Redis cache</b> rather
23
+ # than pointing this at a persistent Redis server (for example, one used as
24
+ # an Active Job queue). Redis won't cope well with mixed usage patterns and it
25
+ # won't expire cache entries by default.
40
26
  #
41
27
  # Redis cache server setup guide: https://redis.io/topics/lru-cache
42
28
  #
43
- # * Supports vanilla Redis, hiredis, and Redis::Distributed.
44
- # * Supports Memcached-like sharding across Redises with Redis::Distributed.
29
+ # * Supports vanilla Redis, hiredis, and +Redis::Distributed+.
30
+ # * Supports Memcached-like sharding across Redises with +Redis::Distributed+.
45
31
  # * Fault tolerant. If the Redis server is unavailable, no exceptions are
46
32
  # raised. Cache fetches are all misses and writes are dropped.
47
33
  # * Local cache. Hot in-memory primary cache within block/middleware scope.
48
- # * +read_multi+ and +write_multi+ support for Redis mget/mset. Use Redis::Distributed
49
- # 4.0.1+ for distributed mget support.
34
+ # * +read_multi+ and +write_multi+ support for Redis mget/mset. Use
35
+ # +Redis::Distributed+ 4.0.1+ for distributed mget support.
50
36
  # * +delete_matched+ support for Redis KEYS globs.
51
37
  class RedisCacheStore < Store
52
- # Keys are truncated with the ActiveSupport digest if they exceed 1kB
38
+ # Keys are truncated with the Active Support digest if they exceed 1kB
53
39
  MAX_KEY_BYTESIZE = 1024
54
40
 
55
41
  DEFAULT_REDIS_OPTIONS = {
56
- connect_timeout: 20,
42
+ connect_timeout: 1,
57
43
  read_timeout: 1,
58
44
  write_timeout: 1,
59
- reconnect_attempts: 0,
60
45
  }
61
46
 
62
47
  DEFAULT_ERROR_HANDLER = -> (method:, returning:, exception:) do
63
48
  if logger
64
49
  logger.error { "RedisCacheStore: #{method} failed, returned #{returning.inspect}: #{exception.class}: #{exception.message}" }
65
50
  end
51
+ ActiveSupport.error_reporter&.report(
52
+ exception,
53
+ severity: :warning,
54
+ source: "redis_cache_store.active_support",
55
+ )
66
56
  end
67
57
 
68
58
  # The maximum number of entries to receive per SCAN call.
@@ -116,13 +106,16 @@ module ActiveSupport
116
106
  end
117
107
  end
118
108
 
119
- attr_reader :redis_options
120
109
  attr_reader :max_key_bytesize
110
+ attr_reader :redis
121
111
 
122
112
  # Creates a new Redis cache store.
123
113
  #
124
- # Handles four options: :redis block, :redis instance, single :url
125
- # string, and multiple :url strings.
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.
126
119
  #
127
120
  # Option Class Result
128
121
  # :redis Proc -> options[:redis].call
@@ -146,34 +139,30 @@ module ActiveSupport
146
139
  # Race condition TTL is not set by default. This can be used to avoid
147
140
  # "thundering herd" cache writes when hot cache entries are expired.
148
141
  # 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
142
+ #
143
+ # Setting <tt>skip_nil: true</tt> will not cache nil results:
144
+ #
145
+ # cache.fetch('foo') { nil }
146
+ # cache.fetch('bar', skip_nil: true) { nil }
147
+ # cache.exist?('foo') # => true
148
+ # cache.exist?('bar') # => false
149
+ def initialize(error_handler: DEFAULT_ERROR_HANDLER, **redis_options)
150
+ universal_options = redis_options.extract!(*UNIVERSAL_OPTIONS)
151
+
152
+ if pool_options = self.class.send(:retrieve_pool_options, redis_options)
153
+ @redis = ::ConnectionPool.new(pool_options) { self.class.build_redis(**redis_options) }
154
+ else
155
+ @redis = self.class.build_redis(**redis_options)
156
+ end
151
157
 
152
158
  @max_key_bytesize = MAX_KEY_BYTESIZE
153
159
  @error_handler = error_handler
154
160
 
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
161
+ super(universal_options)
172
162
  end
173
163
 
174
164
  def inspect
175
- instance = @redis || @redis_options
176
- "#<#{self.class} options=#{options.inspect} redis=#{instance.inspect}>"
165
+ "#<#{self.class} options=#{options.inspect} redis=#{redis.inspect}>"
177
166
  end
178
167
 
179
168
  # Cache Store API implementation.
@@ -181,14 +170,13 @@ module ActiveSupport
181
170
  # Read multiple values at once. Returns a hash of requested keys ->
182
171
  # fetched values.
183
172
  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
173
+ return {} if names.empty?
174
+
175
+ options = names.extract_options!
176
+ instrument_multi(:read_multi, names, options) do |payload|
177
+ read_multi_entries(names, **options).tap do |results|
178
+ payload[:hits] = results.keys
189
179
  end
190
- else
191
- super
192
180
  end
193
181
  end
194
182
 
@@ -212,7 +200,7 @@ module ActiveSupport
212
200
  unless String === matcher
213
201
  raise ArgumentError, "Only Redis glob strings are supported: #{matcher.inspect}"
214
202
  end
215
- redis.with do |c|
203
+ redis.then do |c|
216
204
  pattern = namespace_key(matcher, options)
217
205
  cursor = "0"
218
206
  # Fetch keys in batches using SCAN to avoid blocking the Redis server.
@@ -228,12 +216,21 @@ module ActiveSupport
228
216
  end
229
217
  end
230
218
 
231
- # Cache Store API implementation.
219
+ # Increment a cached integer value using the Redis incrby atomic operator.
220
+ # Returns the updated value.
221
+ #
222
+ # If the key is unset or has expired, it will be set to +amount+:
223
+ #
224
+ # cache.increment("foo") # => 1
225
+ # cache.increment("bar", 100) # => 100
226
+ #
227
+ # To set a specific value, call #write passing <tt>raw: true</tt>:
228
+ #
229
+ # cache.write("baz", 5, raw: true)
230
+ # cache.increment("baz") # => 6
232
231
  #
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.
232
+ # Incrementing a non-numeric value, or a value written without
233
+ # <tt>raw: true</tt>, will fail and return +nil+.
237
234
  #
238
235
  # Failsafe: Raises errors.
239
236
  def increment(name, amount = 1, options = nil)
@@ -241,22 +238,25 @@ module ActiveSupport
241
238
  failsafe :increment do
242
239
  options = merged_options(options)
243
240
  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
241
+ change_counter(key, amount, options)
250
242
  end
251
243
  end
252
244
  end
253
245
 
254
- # Cache Store API implementation.
246
+ # Decrement a cached integer value using the Redis decrby atomic operator.
247
+ # Returns the updated value.
248
+ #
249
+ # If the key is unset or has expired, it will be set to +-amount+:
255
250
  #
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.
251
+ # cache.decrement("foo") # => -1
252
+ #
253
+ # To set a specific value, call #write passing <tt>raw: true</tt>:
254
+ #
255
+ # cache.write("baz", 5, raw: true)
256
+ # cache.decrement("baz") # => 4
257
+ #
258
+ # Decrementing a non-numeric value, or a value written without
259
+ # <tt>raw: true</tt>, will fail and return +nil+.
260
260
  #
261
261
  # Failsafe: Raises errors.
262
262
  def decrement(name, amount = 1, options = nil)
@@ -264,12 +264,7 @@ module ActiveSupport
264
264
  failsafe :decrement do
265
265
  options = merged_options(options)
266
266
  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
267
+ change_counter(key, -amount, options)
273
268
  end
274
269
  end
275
270
  end
@@ -291,38 +286,27 @@ module ActiveSupport
291
286
  if namespace = merged_options(options)[:namespace]
292
287
  delete_matched "*", namespace: namespace
293
288
  else
294
- redis.with { |c| c.flushdb }
289
+ redis.then { |c| c.flushdb }
295
290
  end
296
291
  end
297
292
  end
298
293
 
299
294
  # Get info from redis servers.
300
295
  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
296
+ redis.then { |c| c.info }
312
297
  end
313
298
 
314
299
  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
300
+ def pipeline_entries(entries, &block)
301
+ redis.then { |c|
302
+ if c.is_a?(Redis::Distributed)
303
+ entries.group_by { |k, _v| c.node_for(k) }.each do |node, sub_entries|
304
+ node.pipelined { |pipe| yield(pipe, sub_entries) }
305
+ end
321
306
  else
322
- @mget_capable = true
323
- @mset_capable = true
307
+ c.pipelined { |pipe| yield(pipe, entries) }
324
308
  end
325
- end
309
+ }
326
310
  end
327
311
 
328
312
  # Store provider interface:
@@ -333,35 +317,29 @@ module ActiveSupport
333
317
 
334
318
  def read_serialized_entry(key, raw: false, **options)
335
319
  failsafe :read_entry do
336
- redis.with { |c| c.get(key) }
320
+ redis.then { |c| c.get(key) }
337
321
  end
338
322
  end
339
323
 
340
324
  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
325
  options = merged_options(options)
351
326
  return {} if names == []
352
327
  raw = options&.fetch(:raw, false)
353
328
 
354
329
  keys = names.map { |name| normalize_key(name, options) }
355
330
 
356
- values = failsafe(:read_multi_mget, returning: {}) do
357
- redis.with { |c| c.mget(*keys) }
331
+ values = failsafe(:read_multi_entries, returning: {}) do
332
+ redis.then { |c| c.mget(*keys) }
358
333
  end
359
334
 
360
335
  names.zip(values).each_with_object({}) do |(name, value), results|
361
336
  if value
362
337
  entry = deserialize_entry(value, raw: raw)
363
338
  unless entry.nil? || entry.expired? || entry.mismatched?(normalize_version(name, options))
364
- results[name] = entry.value
339
+ begin
340
+ results[name] = entry.value
341
+ rescue DeserializationError
342
+ end
365
343
  end
366
344
  end
367
345
  end
@@ -374,7 +352,7 @@ module ActiveSupport
374
352
  write_serialized_entry(key, serialize_entry(entry, raw: raw, **options), raw: raw, **options)
375
353
  end
376
354
 
377
- def write_serialized_entry(key, payload, raw: false, unless_exist: false, expires_in: nil, race_condition_ttl: nil, **options)
355
+ def write_serialized_entry(key, payload, raw: false, unless_exist: false, expires_in: nil, race_condition_ttl: nil, pipeline: nil, **options)
378
356
  # If race condition TTL is in use, ensure that cache entries
379
357
  # stick around a bit longer after they would have expired
380
358
  # so we can purposefully serve stale entries.
@@ -388,41 +366,40 @@ module ActiveSupport
388
366
  modifiers[:px] = (1000 * expires_in.to_f).ceil if expires_in
389
367
  end
390
368
 
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
369
+ if pipeline
370
+ pipeline.set(key, payload, **modifiers)
371
+ else
372
+ failsafe :write_entry, returning: false do
373
+ redis.then { |c| c.set key, payload, **modifiers }
374
+ end
399
375
  end
400
376
  end
401
377
 
402
378
  # Delete an entry from the cache.
403
- def delete_entry(key, options)
379
+ def delete_entry(key, **options)
404
380
  failsafe :delete_entry, returning: false do
405
- redis.with { |c| c.del key }
381
+ redis.then { |c| c.del(key) == 1 }
406
382
  end
407
383
  end
408
384
 
409
385
  # Deletes multiple entries in the cache. Returns the number of entries deleted.
410
386
  def delete_multi_entries(entries, **_options)
411
- redis.with { |c| c.del(entries) }
387
+ failsafe :delete_multi_entries, returning: 0 do
388
+ redis.then { |c| c.del(entries) }
389
+ end
412
390
  end
413
391
 
414
392
  # 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
393
+ def write_multi_entries(entries, **options)
394
+ return if entries.empty?
395
+
396
+ failsafe :write_multi_entries do
397
+ pipeline_entries(entries) do |pipeline, sharded_entries|
398
+ options = options.dup
399
+ options[:pipeline] = pipeline
400
+ sharded_entries.each do |key, entry|
401
+ write_entry key, entry, **options
423
402
  end
424
- else
425
- super
426
403
  end
427
404
  end
428
405
  end
@@ -464,10 +441,43 @@ module ActiveSupport
464
441
  end
465
442
  end
466
443
 
444
+ def change_counter(key, amount, options)
445
+ redis.then do |c|
446
+ c = c.node_for(key) if c.is_a?(Redis::Distributed)
447
+
448
+ expires_in = options[:expires_in]
449
+
450
+ if expires_in
451
+ if supports_expire_nx?
452
+ count, _ = c.pipelined do |pipeline|
453
+ pipeline.incrby(key, amount)
454
+ pipeline.call(:expire, key, expires_in.to_i, "NX")
455
+ end
456
+ else
457
+ count, ttl = c.pipelined do |pipeline|
458
+ pipeline.incrby(key, amount)
459
+ pipeline.ttl(key)
460
+ end
461
+ c.expire(key, expires_in.to_i) if ttl < 0
462
+ end
463
+ else
464
+ count = c.incrby(key, amount)
465
+ end
466
+
467
+ count
468
+ end
469
+ end
470
+
471
+ def supports_expire_nx?
472
+ return @supports_expire_nx if defined?(@supports_expire_nx)
473
+
474
+ redis_versions = redis.then { |c| Array.wrap(c.info("server")).pluck("redis_version") }
475
+ @supports_expire_nx = redis_versions.all? { |v| Gem::Version.new(v) >= Gem::Version.new("7.0.0") }
476
+ end
477
+
467
478
  def failsafe(method, returning: nil)
468
479
  yield
469
480
  rescue ::Redis::BaseError => error
470
- ActiveSupport.error_reporter&.report(error, handled: true, severity: :warning)
471
481
  @error_handler&.call(method: method, exception: error, returning: returning)
472
482
  returning
473
483
  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