activesupport 7.0.8.7 → 7.2.2.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 (198) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +143 -459
  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 +3 -1
  7. data/lib/active_support/backtrace_cleaner.rb +39 -7
  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 +49 -17
  14. data/lib/active_support/cache/mem_cache_store.rb +94 -128
  15. data/lib/active_support/cache/memory_store.rb +80 -25
  16. data/lib/active_support/cache/null_store.rb +6 -0
  17. data/lib/active_support/cache/redis_cache_store.rb +165 -152
  18. data/lib/active_support/cache/serializer_with_fallback.rb +152 -0
  19. data/lib/active_support/cache/strategy/local_cache.rb +29 -14
  20. data/lib/active_support/cache.rb +363 -291
  21. data/lib/active_support/callbacks.rb +118 -134
  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 +1 -2
  28. data/lib/active_support/core_ext/array.rb +0 -1
  29. data/lib/active_support/core_ext/class/subclasses.rb +17 -34
  30. data/lib/active_support/core_ext/date/blank.rb +4 -0
  31. data/lib/active_support/core_ext/date/conversions.rb +1 -2
  32. data/lib/active_support/core_ext/date.rb +0 -1
  33. data/lib/active_support/core_ext/date_and_time/calculations.rb +10 -0
  34. data/lib/active_support/core_ext/date_and_time/compatibility.rb +28 -1
  35. data/lib/active_support/core_ext/date_time/blank.rb +4 -0
  36. data/lib/active_support/core_ext/date_time/conversions.rb +2 -2
  37. data/lib/active_support/core_ext/date_time.rb +0 -1
  38. data/lib/active_support/core_ext/digest/uuid.rb +7 -10
  39. data/lib/active_support/core_ext/enumerable.rb +3 -75
  40. data/lib/active_support/core_ext/erb/util.rb +201 -0
  41. data/lib/active_support/core_ext/hash/conversions.rb +1 -1
  42. data/lib/active_support/core_ext/hash/deep_merge.rb +22 -14
  43. data/lib/active_support/core_ext/hash/keys.rb +4 -4
  44. data/lib/active_support/core_ext/module/attr_internal.rb +17 -6
  45. data/lib/active_support/core_ext/module/attribute_accessors.rb +6 -0
  46. data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +34 -16
  47. data/lib/active_support/core_ext/module/concerning.rb +6 -6
  48. data/lib/active_support/core_ext/module/delegation.rb +20 -119
  49. data/lib/active_support/core_ext/module/deprecation.rb +12 -12
  50. data/lib/active_support/core_ext/module/introspection.rb +0 -1
  51. data/lib/active_support/core_ext/numeric/bytes.rb +9 -0
  52. data/lib/active_support/core_ext/numeric/conversions.rb +5 -3
  53. data/lib/active_support/core_ext/numeric.rb +0 -1
  54. data/lib/active_support/core_ext/object/blank.rb +45 -1
  55. data/lib/active_support/core_ext/object/deep_dup.rb +16 -0
  56. data/lib/active_support/core_ext/object/inclusion.rb +13 -5
  57. data/lib/active_support/core_ext/object/instance_variables.rb +4 -2
  58. data/lib/active_support/core_ext/object/json.rb +17 -7
  59. data/lib/active_support/core_ext/object/with.rb +46 -0
  60. data/lib/active_support/core_ext/object/with_options.rb +4 -4
  61. data/lib/active_support/core_ext/object.rb +1 -0
  62. data/lib/active_support/core_ext/pathname/blank.rb +20 -0
  63. data/lib/active_support/core_ext/pathname/existence.rb +2 -0
  64. data/lib/active_support/core_ext/pathname.rb +1 -0
  65. data/lib/active_support/core_ext/range/conversions.rb +28 -7
  66. data/lib/active_support/core_ext/range/overlap.rb +40 -0
  67. data/lib/active_support/core_ext/range.rb +1 -2
  68. data/lib/active_support/core_ext/securerandom.rb +1 -5
  69. data/lib/active_support/core_ext/string/conversions.rb +1 -1
  70. data/lib/active_support/core_ext/string/filters.rb +21 -15
  71. data/lib/active_support/core_ext/string/indent.rb +1 -1
  72. data/lib/active_support/core_ext/string/inflections.rb +16 -5
  73. data/lib/active_support/core_ext/string/multibyte.rb +1 -1
  74. data/lib/active_support/core_ext/string/output_safety.rb +34 -177
  75. data/lib/active_support/core_ext/thread/backtrace/location.rb +12 -0
  76. data/lib/active_support/core_ext/time/calculations.rb +36 -30
  77. data/lib/active_support/core_ext/time/compatibility.rb +16 -0
  78. data/lib/active_support/core_ext/time/conversions.rb +1 -3
  79. data/lib/active_support/core_ext/time/zones.rb +4 -4
  80. data/lib/active_support/core_ext/time.rb +0 -1
  81. data/lib/active_support/core_ext.rb +0 -1
  82. data/lib/active_support/current_attributes.rb +53 -46
  83. data/lib/active_support/deep_mergeable.rb +53 -0
  84. data/lib/active_support/delegation.rb +202 -0
  85. data/lib/active_support/dependencies/autoload.rb +9 -16
  86. data/lib/active_support/deprecation/behaviors.rb +65 -42
  87. data/lib/active_support/deprecation/constant_accessor.rb +47 -25
  88. data/lib/active_support/deprecation/deprecators.rb +104 -0
  89. data/lib/active_support/deprecation/disallowed.rb +3 -5
  90. data/lib/active_support/deprecation/method_wrappers.rb +6 -23
  91. data/lib/active_support/deprecation/proxy_wrappers.rb +34 -22
  92. data/lib/active_support/deprecation/reporting.rb +49 -27
  93. data/lib/active_support/deprecation.rb +39 -9
  94. data/lib/active_support/deprecator.rb +7 -0
  95. data/lib/active_support/descendants_tracker.rb +66 -172
  96. data/lib/active_support/duration/iso8601_parser.rb +2 -2
  97. data/lib/active_support/duration/iso8601_serializer.rb +1 -4
  98. data/lib/active_support/duration.rb +13 -7
  99. data/lib/active_support/encrypted_configuration.rb +30 -9
  100. data/lib/active_support/encrypted_file.rb +9 -4
  101. data/lib/active_support/environment_inquirer.rb +22 -2
  102. data/lib/active_support/error_reporter/test_helper.rb +15 -0
  103. data/lib/active_support/error_reporter.rb +160 -36
  104. data/lib/active_support/evented_file_update_checker.rb +0 -1
  105. data/lib/active_support/execution_wrapper.rb +4 -5
  106. data/lib/active_support/file_update_checker.rb +5 -3
  107. data/lib/active_support/fork_tracker.rb +4 -32
  108. data/lib/active_support/gem_version.rb +4 -4
  109. data/lib/active_support/gzip.rb +2 -0
  110. data/lib/active_support/hash_with_indifferent_access.rb +41 -25
  111. data/lib/active_support/html_safe_translation.rb +19 -6
  112. data/lib/active_support/i18n.rb +1 -1
  113. data/lib/active_support/i18n_railtie.rb +20 -13
  114. data/lib/active_support/inflector/inflections.rb +2 -0
  115. data/lib/active_support/inflector/methods.rb +23 -11
  116. data/lib/active_support/inflector/transliterate.rb +3 -1
  117. data/lib/active_support/isolated_execution_state.rb +26 -22
  118. data/lib/active_support/json/decoding.rb +2 -1
  119. data/lib/active_support/json/encoding.rb +25 -43
  120. data/lib/active_support/key_generator.rb +9 -1
  121. data/lib/active_support/lazy_load_hooks.rb +6 -4
  122. data/lib/active_support/locale/en.yml +2 -0
  123. data/lib/active_support/log_subscriber.rb +74 -34
  124. data/lib/active_support/logger.rb +22 -60
  125. data/lib/active_support/logger_thread_safe_level.rb +10 -32
  126. data/lib/active_support/message_encryptor.rb +197 -53
  127. data/lib/active_support/message_encryptors.rb +141 -0
  128. data/lib/active_support/message_pack/cache_serializer.rb +23 -0
  129. data/lib/active_support/message_pack/extensions.rb +305 -0
  130. data/lib/active_support/message_pack/serializer.rb +63 -0
  131. data/lib/active_support/message_pack.rb +50 -0
  132. data/lib/active_support/message_verifier.rb +220 -89
  133. data/lib/active_support/message_verifiers.rb +135 -0
  134. data/lib/active_support/messages/codec.rb +65 -0
  135. data/lib/active_support/messages/metadata.rb +111 -45
  136. data/lib/active_support/messages/rotation_coordinator.rb +93 -0
  137. data/lib/active_support/messages/rotator.rb +34 -32
  138. data/lib/active_support/messages/serializer_with_fallback.rb +158 -0
  139. data/lib/active_support/multibyte/chars.rb +4 -2
  140. data/lib/active_support/multibyte/unicode.rb +9 -37
  141. data/lib/active_support/notifications/fanout.rb +248 -87
  142. data/lib/active_support/notifications/instrumenter.rb +93 -25
  143. data/lib/active_support/notifications.rb +29 -28
  144. data/lib/active_support/number_helper/number_converter.rb +16 -7
  145. data/lib/active_support/number_helper/number_to_currency_converter.rb +6 -6
  146. data/lib/active_support/number_helper/number_to_human_size_converter.rb +3 -3
  147. data/lib/active_support/number_helper/number_to_phone_converter.rb +1 -0
  148. data/lib/active_support/number_helper.rb +379 -318
  149. data/lib/active_support/option_merger.rb +2 -2
  150. data/lib/active_support/ordered_hash.rb +3 -3
  151. data/lib/active_support/ordered_options.rb +67 -15
  152. data/lib/active_support/parameter_filter.rb +84 -69
  153. data/lib/active_support/proxy_object.rb +8 -3
  154. data/lib/active_support/railtie.rb +25 -20
  155. data/lib/active_support/reloader.rb +12 -4
  156. data/lib/active_support/rescuable.rb +2 -0
  157. data/lib/active_support/secure_compare_rotator.rb +16 -9
  158. data/lib/active_support/string_inquirer.rb +4 -2
  159. data/lib/active_support/subscriber.rb +10 -27
  160. data/lib/active_support/syntax_error_proxy.rb +60 -0
  161. data/lib/active_support/tagged_logging.rb +64 -25
  162. data/lib/active_support/test_case.rb +156 -7
  163. data/lib/active_support/testing/assertions.rb +28 -12
  164. data/lib/active_support/testing/autorun.rb +0 -2
  165. data/lib/active_support/testing/constant_stubbing.rb +54 -0
  166. data/lib/active_support/testing/deprecation.rb +20 -27
  167. data/lib/active_support/testing/error_reporter_assertions.rb +107 -0
  168. data/lib/active_support/testing/isolation.rb +21 -9
  169. data/lib/active_support/testing/method_call_assertions.rb +7 -8
  170. data/lib/active_support/testing/parallelization/server.rb +3 -0
  171. data/lib/active_support/testing/parallelize_executor.rb +8 -3
  172. data/lib/active_support/testing/setup_and_teardown.rb +2 -0
  173. data/lib/active_support/testing/stream.rb +1 -1
  174. data/lib/active_support/testing/strict_warnings.rb +43 -0
  175. data/lib/active_support/testing/tests_without_assertions.rb +19 -0
  176. data/lib/active_support/testing/time_helpers.rb +38 -16
  177. data/lib/active_support/time_with_zone.rb +12 -18
  178. data/lib/active_support/values/time_zone.rb +25 -14
  179. data/lib/active_support/version.rb +1 -1
  180. data/lib/active_support/xml_mini/jdom.rb +3 -10
  181. data/lib/active_support/xml_mini/nokogiri.rb +1 -1
  182. data/lib/active_support/xml_mini/nokogirisax.rb +1 -1
  183. data/lib/active_support/xml_mini/rexml.rb +1 -1
  184. data/lib/active_support/xml_mini.rb +12 -3
  185. data/lib/active_support.rb +15 -3
  186. metadata +140 -19
  187. data/lib/active_support/core_ext/array/deprecated_conversions.rb +0 -25
  188. data/lib/active_support/core_ext/date/deprecated_conversions.rb +0 -40
  189. data/lib/active_support/core_ext/date_time/deprecated_conversions.rb +0 -36
  190. data/lib/active_support/core_ext/numeric/deprecated_conversions.rb +0 -60
  191. data/lib/active_support/core_ext/range/deprecated_conversions.rb +0 -36
  192. data/lib/active_support/core_ext/range/include_time_with_zone.rb +0 -5
  193. data/lib/active_support/core_ext/range/overlaps.rb +0 -10
  194. data/lib/active_support/core_ext/time/deprecated_conversions.rb +0 -73
  195. data/lib/active_support/core_ext/uri.rb +0 -5
  196. data/lib/active_support/deprecation/instance_delegator.rb +0 -38
  197. data/lib/active_support/per_thread_registry.rb +0 -65
  198. data/lib/active_support/ruby_features.rb +0 -7
@@ -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
 
@@ -208,12 +196,13 @@ module ActiveSupport
208
196
  #
209
197
  # Failsafe: Raises errors.
210
198
  def delete_matched(matcher, options = nil)
211
- instrument :delete_matched, matcher do
212
- unless String === matcher
213
- raise ArgumentError, "Only Redis glob strings are supported: #{matcher.inspect}"
214
- end
215
- redis.with do |c|
216
- pattern = namespace_key(matcher, options)
199
+ unless String === matcher
200
+ raise ArgumentError, "Only Redis glob strings are supported: #{matcher.inspect}"
201
+ end
202
+ pattern = namespace_key(matcher, options)
203
+
204
+ instrument :delete_matched, pattern do
205
+ redis.then do |c|
217
206
  cursor = "0"
218
207
  # Fetch keys in batches using SCAN to avoid blocking the Redis server.
219
208
  nodes = c.respond_to?(:nodes) ? c.nodes : [c]
@@ -228,48 +217,57 @@ module ActiveSupport
228
217
  end
229
218
  end
230
219
 
231
- # Cache Store API implementation.
220
+ # Increment a cached integer value using the Redis incrby atomic operator.
221
+ # Returns the updated value.
222
+ #
223
+ # If the key is unset or has expired, it will be set to +amount+:
224
+ #
225
+ # cache.increment("foo") # => 1
226
+ # cache.increment("bar", 100) # => 100
227
+ #
228
+ # To set a specific value, call #write passing <tt>raw: true</tt>:
232
229
  #
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.
230
+ # cache.write("baz", 5, raw: true)
231
+ # cache.increment("baz") # => 6
232
+ #
233
+ # Incrementing a non-numeric value, or a value written without
234
+ # <tt>raw: true</tt>, will fail and return +nil+.
237
235
  #
238
236
  # Failsafe: Raises errors.
239
237
  def increment(name, amount = 1, options = nil)
240
- instrument :increment, name, amount: amount do
241
- failsafe :increment do
242
- options = merged_options(options)
243
- key = normalize_key(name, options)
238
+ options = merged_options(options)
239
+ key = normalize_key(name, options)
244
240
 
245
- redis.with do |c|
246
- c.incrby(key, amount).tap do
247
- write_key_expiry(c, key, options)
248
- end
249
- end
241
+ instrument :increment, key, amount: amount do
242
+ failsafe :increment do
243
+ change_counter(key, amount, options)
250
244
  end
251
245
  end
252
246
  end
253
247
 
254
- # Cache Store API implementation.
248
+ # Decrement a cached integer value using the Redis decrby atomic operator.
249
+ # Returns the updated value.
250
+ #
251
+ # If the key is unset or has expired, it will be set to +-amount+:
252
+ #
253
+ # cache.decrement("foo") # => -1
254
+ #
255
+ # To set a specific value, call #write passing <tt>raw: true</tt>:
255
256
  #
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.
257
+ # cache.write("baz", 5, raw: true)
258
+ # cache.decrement("baz") # => 4
259
+ #
260
+ # Decrementing a non-numeric value, or a value written without
261
+ # <tt>raw: true</tt>, will fail and return +nil+.
260
262
  #
261
263
  # Failsafe: Raises errors.
262
264
  def decrement(name, amount = 1, options = nil)
263
- instrument :decrement, name, amount: amount do
264
- failsafe :decrement do
265
- options = merged_options(options)
266
- key = normalize_key(name, options)
265
+ options = merged_options(options)
266
+ key = normalize_key(name, options)
267
267
 
268
- redis.with do |c|
269
- c.decrby(key, amount).tap do
270
- write_key_expiry(c, key, options)
271
- end
272
- end
268
+ instrument :decrement, key, amount: amount do
269
+ failsafe :decrement do
270
+ change_counter(key, -amount, options)
273
271
  end
274
272
  end
275
273
  end
@@ -291,38 +289,27 @@ module ActiveSupport
291
289
  if namespace = merged_options(options)[:namespace]
292
290
  delete_matched "*", namespace: namespace
293
291
  else
294
- redis.with { |c| c.flushdb }
292
+ redis.then { |c| c.flushdb }
295
293
  end
296
294
  end
297
295
  end
298
296
 
299
297
  # Get info from redis servers.
300
298
  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
299
+ redis.then { |c| c.info }
312
300
  end
313
301
 
314
302
  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
303
+ def pipeline_entries(entries, &block)
304
+ redis.then { |c|
305
+ if c.is_a?(Redis::Distributed)
306
+ entries.group_by { |k, _v| c.node_for(k) }.each do |node, sub_entries|
307
+ node.pipelined { |pipe| yield(pipe, sub_entries) }
308
+ end
321
309
  else
322
- @mget_capable = true
323
- @mset_capable = true
310
+ c.pipelined { |pipe| yield(pipe, entries) }
324
311
  end
325
- end
312
+ }
326
313
  end
327
314
 
328
315
  # Store provider interface:
@@ -333,35 +320,29 @@ module ActiveSupport
333
320
 
334
321
  def read_serialized_entry(key, raw: false, **options)
335
322
  failsafe :read_entry do
336
- redis.with { |c| c.get(key) }
323
+ redis.then { |c| c.get(key) }
337
324
  end
338
325
  end
339
326
 
340
327
  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
328
  options = merged_options(options)
351
329
  return {} if names == []
352
330
  raw = options&.fetch(:raw, false)
353
331
 
354
332
  keys = names.map { |name| normalize_key(name, options) }
355
333
 
356
- values = failsafe(:read_multi_mget, returning: {}) do
357
- redis.with { |c| c.mget(*keys) }
334
+ values = failsafe(:read_multi_entries, returning: {}) do
335
+ redis.then { |c| c.mget(*keys) }
358
336
  end
359
337
 
360
338
  names.zip(values).each_with_object({}) do |(name, value), results|
361
339
  if value
362
340
  entry = deserialize_entry(value, raw: raw)
363
341
  unless entry.nil? || entry.expired? || entry.mismatched?(normalize_version(name, options))
364
- results[name] = entry.value
342
+ begin
343
+ results[name] = entry.value
344
+ rescue DeserializationError
345
+ end
365
346
  end
366
347
  end
367
348
  end
@@ -374,7 +355,7 @@ module ActiveSupport
374
355
  write_serialized_entry(key, serialize_entry(entry, raw: raw, **options), raw: raw, **options)
375
356
  end
376
357
 
377
- def write_serialized_entry(key, payload, raw: false, unless_exist: false, expires_in: nil, race_condition_ttl: nil, **options)
358
+ def write_serialized_entry(key, payload, raw: false, unless_exist: false, expires_in: nil, race_condition_ttl: nil, pipeline: nil, **options)
378
359
  # If race condition TTL is in use, ensure that cache entries
379
360
  # stick around a bit longer after they would have expired
380
361
  # so we can purposefully serve stale entries.
@@ -388,41 +369,40 @@ module ActiveSupport
388
369
  modifiers[:px] = (1000 * expires_in.to_f).ceil if expires_in
389
370
  end
390
371
 
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
372
+ if pipeline
373
+ pipeline.set(key, payload, **modifiers)
374
+ else
375
+ failsafe :write_entry, returning: nil do
376
+ redis.then { |c| !!c.set(key, payload, **modifiers) }
377
+ end
399
378
  end
400
379
  end
401
380
 
402
381
  # Delete an entry from the cache.
403
- def delete_entry(key, options)
382
+ def delete_entry(key, **options)
404
383
  failsafe :delete_entry, returning: false do
405
- redis.with { |c| c.del key }
384
+ redis.then { |c| c.del(key) == 1 }
406
385
  end
407
386
  end
408
387
 
409
388
  # Deletes multiple entries in the cache. Returns the number of entries deleted.
410
389
  def delete_multi_entries(entries, **_options)
411
- redis.with { |c| c.del(entries) }
390
+ failsafe :delete_multi_entries, returning: 0 do
391
+ redis.then { |c| c.del(entries) }
392
+ end
412
393
  end
413
394
 
414
395
  # 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
396
+ def write_multi_entries(entries, **options)
397
+ return if entries.empty?
398
+
399
+ failsafe :write_multi_entries do
400
+ pipeline_entries(entries) do |pipeline, sharded_entries|
401
+ options = options.dup
402
+ options[:pipeline] = pipeline
403
+ sharded_entries.each do |key, entry|
404
+ write_entry key, entry, **options
423
405
  end
424
- else
425
- super
426
406
  end
427
407
  end
428
408
  end
@@ -464,10 +444,43 @@ module ActiveSupport
464
444
  end
465
445
  end
466
446
 
447
+ def change_counter(key, amount, options)
448
+ redis.then do |c|
449
+ c = c.node_for(key) if c.is_a?(Redis::Distributed)
450
+
451
+ expires_in = options[:expires_in]
452
+
453
+ if expires_in
454
+ if supports_expire_nx?
455
+ count, _ = c.pipelined do |pipeline|
456
+ pipeline.incrby(key, amount)
457
+ pipeline.call(:expire, key, expires_in.to_i, "NX")
458
+ end
459
+ else
460
+ count, ttl = c.pipelined do |pipeline|
461
+ pipeline.incrby(key, amount)
462
+ pipeline.ttl(key)
463
+ end
464
+ c.expire(key, expires_in.to_i) if ttl < 0
465
+ end
466
+ else
467
+ count = c.incrby(key, amount)
468
+ end
469
+
470
+ count
471
+ end
472
+ end
473
+
474
+ def supports_expire_nx?
475
+ return @supports_expire_nx if defined?(@supports_expire_nx)
476
+
477
+ redis_versions = redis.then { |c| Array.wrap(c.info("server")).pluck("redis_version") }
478
+ @supports_expire_nx = redis_versions.all? { |v| Gem::Version.new(v) >= Gem::Version.new("7.0.0") }
479
+ end
480
+
467
481
  def failsafe(method, returning: nil)
468
482
  yield
469
483
  rescue ::Redis::BaseError => error
470
- ActiveSupport.error_reporter&.report(error, handled: true, severity: :warning)
471
484
  @error_handler&.call(method: method, exception: error, returning: returning)
472
485
  returning
473
486
  end
@@ -0,0 +1,152 @@
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 Marshal70WithFallback
67
+ include SerializerWithFallback
68
+ extend self
69
+
70
+ MARK_UNCOMPRESSED = "\x00".b.freeze
71
+ MARK_COMPRESSED = "\x01".b.freeze
72
+
73
+ def dump(entry)
74
+ MARK_UNCOMPRESSED + Marshal.dump(entry.pack)
75
+ end
76
+
77
+ def dump_compressed(entry, threshold)
78
+ dumped = Marshal.dump(entry.pack)
79
+
80
+ if dumped.bytesize >= threshold
81
+ compressed = Zlib::Deflate.deflate(dumped)
82
+ return MARK_COMPRESSED + compressed if compressed.bytesize < dumped.bytesize
83
+ end
84
+
85
+ MARK_UNCOMPRESSED + dumped
86
+ end
87
+
88
+ def _load(marked)
89
+ dumped = marked.byteslice(1..-1)
90
+ dumped = Zlib::Inflate.inflate(dumped) if marked.start_with?(MARK_COMPRESSED)
91
+ Cache::Entry.unpack(marshal_load(dumped))
92
+ end
93
+
94
+ def dumped?(dumped)
95
+ dumped.start_with?(MARK_UNCOMPRESSED, MARK_COMPRESSED)
96
+ end
97
+ end
98
+
99
+ module Marshal71WithFallback
100
+ include SerializerWithFallback
101
+ extend self
102
+
103
+ MARSHAL_SIGNATURE = "\x04\x08".b.freeze
104
+
105
+ def dump(value)
106
+ Marshal.dump(value)
107
+ end
108
+
109
+ def _load(dumped)
110
+ marshal_load(dumped)
111
+ end
112
+
113
+ def dumped?(dumped)
114
+ dumped.start_with?(MARSHAL_SIGNATURE)
115
+ end
116
+ end
117
+
118
+ module MessagePackWithFallback
119
+ include SerializerWithFallback
120
+ extend self
121
+
122
+ def dump(value)
123
+ ActiveSupport::MessagePack::CacheSerializer.dump(value)
124
+ end
125
+
126
+ def _load(dumped)
127
+ ActiveSupport::MessagePack::CacheSerializer.load(dumped)
128
+ end
129
+
130
+ def dumped?(dumped)
131
+ available? && ActiveSupport::MessagePack.signature?(dumped)
132
+ end
133
+
134
+ private
135
+ def available?
136
+ return @available if defined?(@available)
137
+ silence_warnings { require "active_support/message_pack" }
138
+ @available = true
139
+ rescue LoadError
140
+ @available = false
141
+ end
142
+ end
143
+
144
+ SERIALIZERS = {
145
+ passthrough: PassthroughWithFallback,
146
+ marshal_7_0: Marshal70WithFallback,
147
+ marshal_7_1: Marshal71WithFallback,
148
+ message_pack: MessagePackWithFallback,
149
+ }
150
+ end
151
+ end
152
+ end