activesupport 7.0.8.7 → 7.1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (181) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +995 -294
  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 +30 -5
  8. data/lib/active_support/benchmarkable.rb +1 -0
  9. data/lib/active_support/broadcast_logger.rb +251 -0
  10. data/lib/active_support/builder.rb +1 -1
  11. data/lib/active_support/cache/coder.rb +153 -0
  12. data/lib/active_support/cache/entry.rb +134 -0
  13. data/lib/active_support/cache/file_store.rb +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 +333 -253
  21. data/lib/active_support/callbacks.rb +44 -21
  22. data/lib/active_support/code_generator.rb +15 -10
  23. data/lib/active_support/concern.rb +4 -2
  24. data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +42 -3
  25. data/lib/active_support/concurrency/null_lock.rb +13 -0
  26. data/lib/active_support/configurable.rb +10 -0
  27. data/lib/active_support/core_ext/array/conversions.rb +2 -1
  28. data/lib/active_support/core_ext/array.rb +0 -1
  29. data/lib/active_support/core_ext/class/subclasses.rb +13 -10
  30. data/lib/active_support/core_ext/date/conversions.rb +2 -1
  31. data/lib/active_support/core_ext/date.rb +0 -1
  32. data/lib/active_support/core_ext/date_and_time/calculations.rb +10 -0
  33. data/lib/active_support/core_ext/date_time/conversions.rb +6 -2
  34. data/lib/active_support/core_ext/date_time.rb +0 -1
  35. data/lib/active_support/core_ext/digest/uuid.rb +1 -10
  36. data/lib/active_support/core_ext/enumerable.rb +3 -75
  37. data/lib/active_support/core_ext/erb/util.rb +196 -0
  38. data/lib/active_support/core_ext/hash/conversions.rb +1 -1
  39. data/lib/active_support/core_ext/hash/deep_merge.rb +22 -14
  40. data/lib/active_support/core_ext/module/attribute_accessors.rb +6 -0
  41. data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +34 -16
  42. data/lib/active_support/core_ext/module/concerning.rb +6 -6
  43. data/lib/active_support/core_ext/module/delegation.rb +81 -37
  44. data/lib/active_support/core_ext/module/deprecation.rb +15 -12
  45. data/lib/active_support/core_ext/module/introspection.rb +0 -1
  46. data/lib/active_support/core_ext/numeric/bytes.rb +9 -0
  47. data/lib/active_support/core_ext/numeric/conversions.rb +2 -0
  48. data/lib/active_support/core_ext/numeric.rb +0 -1
  49. data/lib/active_support/core_ext/object/deep_dup.rb +16 -0
  50. data/lib/active_support/core_ext/object/inclusion.rb +13 -5
  51. data/lib/active_support/core_ext/object/instance_variables.rb +22 -12
  52. data/lib/active_support/core_ext/object/json.rb +16 -6
  53. data/lib/active_support/core_ext/object/with.rb +44 -0
  54. data/lib/active_support/core_ext/object/with_options.rb +4 -4
  55. data/lib/active_support/core_ext/object.rb +1 -0
  56. data/lib/active_support/core_ext/pathname/blank.rb +16 -0
  57. data/lib/active_support/core_ext/pathname/existence.rb +2 -0
  58. data/lib/active_support/core_ext/pathname.rb +1 -0
  59. data/lib/active_support/core_ext/range/conversions.rb +28 -7
  60. data/lib/active_support/core_ext/range/overlap.rb +40 -0
  61. data/lib/active_support/core_ext/range.rb +1 -2
  62. data/lib/active_support/core_ext/securerandom.rb +24 -12
  63. data/lib/active_support/core_ext/string/filters.rb +20 -14
  64. data/lib/active_support/core_ext/string/indent.rb +1 -1
  65. data/lib/active_support/core_ext/string/inflections.rb +16 -5
  66. data/lib/active_support/core_ext/string/output_safety.rb +38 -174
  67. data/lib/active_support/core_ext/thread/backtrace/location.rb +12 -0
  68. data/lib/active_support/core_ext/time/calculations.rb +18 -2
  69. data/lib/active_support/core_ext/time/conversions.rb +2 -2
  70. data/lib/active_support/core_ext/time/zones.rb +4 -4
  71. data/lib/active_support/core_ext/time.rb +0 -1
  72. data/lib/active_support/current_attributes.rb +15 -6
  73. data/lib/active_support/deep_mergeable.rb +53 -0
  74. data/lib/active_support/dependencies/autoload.rb +17 -12
  75. data/lib/active_support/deprecation/behaviors.rb +65 -42
  76. data/lib/active_support/deprecation/constant_accessor.rb +5 -4
  77. data/lib/active_support/deprecation/deprecators.rb +104 -0
  78. data/lib/active_support/deprecation/disallowed.rb +3 -5
  79. data/lib/active_support/deprecation/instance_delegator.rb +31 -4
  80. data/lib/active_support/deprecation/method_wrappers.rb +6 -23
  81. data/lib/active_support/deprecation/proxy_wrappers.rb +37 -22
  82. data/lib/active_support/deprecation/reporting.rb +43 -26
  83. data/lib/active_support/deprecation.rb +32 -5
  84. data/lib/active_support/deprecator.rb +7 -0
  85. data/lib/active_support/descendants_tracker.rb +104 -132
  86. data/lib/active_support/duration/iso8601_serializer.rb +0 -2
  87. data/lib/active_support/duration.rb +2 -1
  88. data/lib/active_support/encrypted_configuration.rb +30 -9
  89. data/lib/active_support/encrypted_file.rb +8 -3
  90. data/lib/active_support/environment_inquirer.rb +22 -2
  91. data/lib/active_support/error_reporter/test_helper.rb +15 -0
  92. data/lib/active_support/error_reporter.rb +121 -35
  93. data/lib/active_support/execution_wrapper.rb +4 -4
  94. data/lib/active_support/file_update_checker.rb +4 -2
  95. data/lib/active_support/fork_tracker.rb +10 -2
  96. data/lib/active_support/gem_version.rb +4 -4
  97. data/lib/active_support/gzip.rb +2 -0
  98. data/lib/active_support/hash_with_indifferent_access.rb +35 -17
  99. data/lib/active_support/html_safe_translation.rb +16 -6
  100. data/lib/active_support/i18n.rb +1 -1
  101. data/lib/active_support/i18n_railtie.rb +20 -13
  102. data/lib/active_support/inflector/inflections.rb +2 -0
  103. data/lib/active_support/inflector/methods.rb +23 -11
  104. data/lib/active_support/inflector/transliterate.rb +3 -1
  105. data/lib/active_support/isolated_execution_state.rb +26 -22
  106. data/lib/active_support/json/decoding.rb +2 -1
  107. data/lib/active_support/json/encoding.rb +25 -43
  108. data/lib/active_support/key_generator.rb +9 -1
  109. data/lib/active_support/lazy_load_hooks.rb +6 -4
  110. data/lib/active_support/locale/en.yml +2 -0
  111. data/lib/active_support/log_subscriber.rb +85 -33
  112. data/lib/active_support/logger.rb +9 -60
  113. data/lib/active_support/logger_thread_safe_level.rb +10 -24
  114. data/lib/active_support/message_encryptor.rb +197 -53
  115. data/lib/active_support/message_encryptors.rb +141 -0
  116. data/lib/active_support/message_pack/cache_serializer.rb +23 -0
  117. data/lib/active_support/message_pack/extensions.rb +292 -0
  118. data/lib/active_support/message_pack/serializer.rb +63 -0
  119. data/lib/active_support/message_pack.rb +50 -0
  120. data/lib/active_support/message_verifier.rb +212 -93
  121. data/lib/active_support/message_verifiers.rb +135 -0
  122. data/lib/active_support/messages/codec.rb +65 -0
  123. data/lib/active_support/messages/metadata.rb +111 -45
  124. data/lib/active_support/messages/rotation_coordinator.rb +93 -0
  125. data/lib/active_support/messages/rotator.rb +34 -32
  126. data/lib/active_support/messages/serializer_with_fallback.rb +158 -0
  127. data/lib/active_support/multibyte/chars.rb +2 -0
  128. data/lib/active_support/multibyte/unicode.rb +9 -37
  129. data/lib/active_support/notifications/fanout.rb +245 -81
  130. data/lib/active_support/notifications/instrumenter.rb +87 -22
  131. data/lib/active_support/notifications.rb +1 -1
  132. data/lib/active_support/number_helper/number_converter.rb +14 -5
  133. data/lib/active_support/number_helper/number_to_currency_converter.rb +6 -6
  134. data/lib/active_support/number_helper/number_to_human_size_converter.rb +3 -3
  135. data/lib/active_support/number_helper/number_to_phone_converter.rb +1 -0
  136. data/lib/active_support/number_helper.rb +379 -318
  137. data/lib/active_support/ordered_hash.rb +3 -3
  138. data/lib/active_support/ordered_options.rb +14 -0
  139. data/lib/active_support/parameter_filter.rb +84 -69
  140. data/lib/active_support/proxy_object.rb +2 -0
  141. data/lib/active_support/railtie.rb +33 -21
  142. data/lib/active_support/reloader.rb +12 -4
  143. data/lib/active_support/rescuable.rb +2 -0
  144. data/lib/active_support/secure_compare_rotator.rb +16 -9
  145. data/lib/active_support/string_inquirer.rb +3 -1
  146. data/lib/active_support/subscriber.rb +9 -27
  147. data/lib/active_support/syntax_error_proxy.rb +60 -0
  148. data/lib/active_support/tagged_logging.rb +64 -24
  149. data/lib/active_support/test_case.rb +153 -6
  150. data/lib/active_support/testing/assertions.rb +26 -10
  151. data/lib/active_support/testing/autorun.rb +0 -2
  152. data/lib/active_support/testing/constant_stubbing.rb +32 -0
  153. data/lib/active_support/testing/deprecation.rb +25 -25
  154. data/lib/active_support/testing/error_reporter_assertions.rb +107 -0
  155. data/lib/active_support/testing/isolation.rb +1 -1
  156. data/lib/active_support/testing/method_call_assertions.rb +21 -8
  157. data/lib/active_support/testing/parallelize_executor.rb +8 -3
  158. data/lib/active_support/testing/setup_and_teardown.rb +2 -0
  159. data/lib/active_support/testing/stream.rb +1 -1
  160. data/lib/active_support/testing/strict_warnings.rb +39 -0
  161. data/lib/active_support/testing/time_helpers.rb +37 -15
  162. data/lib/active_support/time_with_zone.rb +4 -14
  163. data/lib/active_support/values/time_zone.rb +18 -7
  164. data/lib/active_support/version.rb +1 -1
  165. data/lib/active_support/xml_mini/jdom.rb +3 -10
  166. data/lib/active_support/xml_mini/nokogiri.rb +1 -1
  167. data/lib/active_support/xml_mini/nokogirisax.rb +1 -1
  168. data/lib/active_support/xml_mini/rexml.rb +1 -1
  169. data/lib/active_support/xml_mini.rb +2 -2
  170. data/lib/active_support.rb +14 -3
  171. metadata +143 -14
  172. data/lib/active_support/core_ext/array/deprecated_conversions.rb +0 -25
  173. data/lib/active_support/core_ext/date/deprecated_conversions.rb +0 -40
  174. data/lib/active_support/core_ext/date_time/deprecated_conversions.rb +0 -36
  175. data/lib/active_support/core_ext/numeric/deprecated_conversions.rb +0 -60
  176. data/lib/active_support/core_ext/range/deprecated_conversions.rb +0 -36
  177. data/lib/active_support/core_ext/range/include_time_with_zone.rb +0 -5
  178. data/lib/active_support/core_ext/range/overlaps.rb +0 -10
  179. data/lib/active_support/core_ext/time/deprecated_conversions.rb +0 -73
  180. data/lib/active_support/core_ext/uri.rb +0 -5
  181. 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