activesupport 7.0.4.3 → 7.2.4

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 (210) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +270 -266
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +7 -7
  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 +238 -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 +51 -19
  14. data/lib/active_support/cache/mem_cache_store.rb +101 -134
  15. data/lib/active_support/cache/memory_store.rb +85 -30
  16. data/lib/active_support/cache/null_store.rb +8 -2
  17. data/lib/active_support/cache/redis_cache_store.rb +168 -153
  18. data/lib/active_support/cache/serializer_with_fallback.rb +152 -0
  19. data/lib/active_support/cache/strategy/local_cache.rb +64 -13
  20. data/lib/active_support/cache.rb +364 -292
  21. data/lib/active_support/callbacks.rb +121 -136
  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/benchmark.rb +1 -0
  30. data/lib/active_support/core_ext/class/attribute.rb +2 -2
  31. data/lib/active_support/core_ext/class/subclasses.rb +17 -34
  32. data/lib/active_support/core_ext/date/blank.rb +4 -0
  33. data/lib/active_support/core_ext/date/calculations.rb +15 -0
  34. data/lib/active_support/core_ext/date/conversions.rb +1 -2
  35. data/lib/active_support/core_ext/date.rb +0 -1
  36. data/lib/active_support/core_ext/date_and_time/calculations.rb +10 -0
  37. data/lib/active_support/core_ext/date_and_time/compatibility.rb +28 -1
  38. data/lib/active_support/core_ext/date_time/blank.rb +4 -0
  39. data/lib/active_support/core_ext/date_time/calculations.rb +4 -0
  40. data/lib/active_support/core_ext/date_time/conversions.rb +6 -4
  41. data/lib/active_support/core_ext/date_time.rb +0 -1
  42. data/lib/active_support/core_ext/digest/uuid.rb +7 -10
  43. data/lib/active_support/core_ext/enumerable.rb +25 -80
  44. data/lib/active_support/core_ext/erb/util.rb +201 -0
  45. data/lib/active_support/core_ext/hash/conversions.rb +1 -1
  46. data/lib/active_support/core_ext/hash/deep_merge.rb +22 -14
  47. data/lib/active_support/core_ext/hash/deep_transform_values.rb +3 -3
  48. data/lib/active_support/core_ext/hash/keys.rb +7 -7
  49. data/lib/active_support/core_ext/integer/inflections.rb +12 -12
  50. data/lib/active_support/core_ext/module/attr_internal.rb +17 -6
  51. data/lib/active_support/core_ext/module/attribute_accessors.rb +6 -0
  52. data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +34 -16
  53. data/lib/active_support/core_ext/module/concerning.rb +6 -6
  54. data/lib/active_support/core_ext/module/delegation.rb +20 -119
  55. data/lib/active_support/core_ext/module/deprecation.rb +12 -12
  56. data/lib/active_support/core_ext/module/introspection.rb +3 -1
  57. data/lib/active_support/core_ext/numeric/bytes.rb +9 -0
  58. data/lib/active_support/core_ext/numeric/conversions.rb +5 -3
  59. data/lib/active_support/core_ext/numeric.rb +0 -1
  60. data/lib/active_support/core_ext/object/blank.rb +45 -1
  61. data/lib/active_support/core_ext/object/deep_dup.rb +16 -0
  62. data/lib/active_support/core_ext/object/duplicable.rb +25 -16
  63. data/lib/active_support/core_ext/object/inclusion.rb +13 -5
  64. data/lib/active_support/core_ext/object/instance_variables.rb +4 -2
  65. data/lib/active_support/core_ext/object/json.rb +18 -8
  66. data/lib/active_support/core_ext/object/to_query.rb +0 -2
  67. data/lib/active_support/core_ext/object/try.rb +2 -2
  68. data/lib/active_support/core_ext/object/with.rb +46 -0
  69. data/lib/active_support/core_ext/object/with_options.rb +9 -9
  70. data/lib/active_support/core_ext/object.rb +1 -0
  71. data/lib/active_support/core_ext/pathname/blank.rb +20 -0
  72. data/lib/active_support/core_ext/pathname/existence.rb +2 -0
  73. data/lib/active_support/core_ext/pathname.rb +1 -0
  74. data/lib/active_support/core_ext/range/conversions.rb +28 -7
  75. data/lib/active_support/core_ext/range/overlap.rb +40 -0
  76. data/lib/active_support/core_ext/range/sole.rb +17 -0
  77. data/lib/active_support/core_ext/range.rb +2 -2
  78. data/lib/active_support/core_ext/securerandom.rb +24 -12
  79. data/lib/active_support/core_ext/string/conversions.rb +1 -1
  80. data/lib/active_support/core_ext/string/filters.rb +24 -18
  81. data/lib/active_support/core_ext/string/indent.rb +1 -1
  82. data/lib/active_support/core_ext/string/inflections.rb +16 -9
  83. data/lib/active_support/core_ext/string/multibyte.rb +3 -3
  84. data/lib/active_support/core_ext/string/output_safety.rb +37 -178
  85. data/lib/active_support/core_ext/thread/backtrace/location.rb +12 -0
  86. data/lib/active_support/core_ext/time/calculations.rb +40 -30
  87. data/lib/active_support/core_ext/time/compatibility.rb +24 -0
  88. data/lib/active_support/core_ext/time/conversions.rb +1 -3
  89. data/lib/active_support/core_ext/time/zones.rb +7 -8
  90. data/lib/active_support/core_ext/time.rb +0 -1
  91. data/lib/active_support/core_ext.rb +0 -1
  92. data/lib/active_support/current_attributes.rb +60 -46
  93. data/lib/active_support/deep_mergeable.rb +53 -0
  94. data/lib/active_support/delegation.rb +202 -0
  95. data/lib/active_support/dependencies/autoload.rb +9 -16
  96. data/lib/active_support/deprecation/behaviors.rb +65 -42
  97. data/lib/active_support/deprecation/constant_accessor.rb +47 -25
  98. data/lib/active_support/deprecation/deprecators.rb +104 -0
  99. data/lib/active_support/deprecation/disallowed.rb +6 -8
  100. data/lib/active_support/deprecation/method_wrappers.rb +6 -23
  101. data/lib/active_support/deprecation/proxy_wrappers.rb +34 -22
  102. data/lib/active_support/deprecation/reporting.rb +49 -27
  103. data/lib/active_support/deprecation.rb +39 -9
  104. data/lib/active_support/deprecator.rb +7 -0
  105. data/lib/active_support/descendants_tracker.rb +66 -172
  106. data/lib/active_support/duration/iso8601_parser.rb +2 -2
  107. data/lib/active_support/duration/iso8601_serializer.rb +1 -4
  108. data/lib/active_support/duration.rb +13 -7
  109. data/lib/active_support/encrypted_configuration.rb +63 -11
  110. data/lib/active_support/encrypted_file.rb +17 -13
  111. data/lib/active_support/environment_inquirer.rb +22 -2
  112. data/lib/active_support/error_reporter/test_helper.rb +15 -0
  113. data/lib/active_support/error_reporter.rb +163 -36
  114. data/lib/active_support/evented_file_update_checker.rb +17 -3
  115. data/lib/active_support/execution_wrapper.rb +5 -6
  116. data/lib/active_support/file_update_checker.rb +6 -4
  117. data/lib/active_support/fork_tracker.rb +4 -32
  118. data/lib/active_support/gem_version.rb +3 -3
  119. data/lib/active_support/gzip.rb +2 -0
  120. data/lib/active_support/hash_with_indifferent_access.rb +50 -30
  121. data/lib/active_support/html_safe_translation.rb +19 -6
  122. data/lib/active_support/i18n.rb +1 -1
  123. data/lib/active_support/i18n_railtie.rb +20 -13
  124. data/lib/active_support/inflector/inflections.rb +2 -0
  125. data/lib/active_support/inflector/methods.rb +30 -20
  126. data/lib/active_support/inflector/transliterate.rb +3 -1
  127. data/lib/active_support/isolated_execution_state.rb +26 -22
  128. data/lib/active_support/json/decoding.rb +4 -3
  129. data/lib/active_support/json/encoding.rb +49 -49
  130. data/lib/active_support/key_generator.rb +9 -1
  131. data/lib/active_support/lazy_load_hooks.rb +8 -6
  132. data/lib/active_support/locale/en.yml +2 -0
  133. data/lib/active_support/log_subscriber.rb +74 -34
  134. data/lib/active_support/logger.rb +22 -60
  135. data/lib/active_support/logger_thread_safe_level.rb +10 -32
  136. data/lib/active_support/message_encryptor.rb +197 -53
  137. data/lib/active_support/message_encryptors.rb +141 -0
  138. data/lib/active_support/message_pack/cache_serializer.rb +23 -0
  139. data/lib/active_support/message_pack/extensions.rb +305 -0
  140. data/lib/active_support/message_pack/serializer.rb +63 -0
  141. data/lib/active_support/message_pack.rb +50 -0
  142. data/lib/active_support/message_verifier.rb +229 -89
  143. data/lib/active_support/message_verifiers.rb +137 -0
  144. data/lib/active_support/messages/codec.rb +65 -0
  145. data/lib/active_support/messages/metadata.rb +111 -45
  146. data/lib/active_support/messages/rotation_coordinator.rb +93 -0
  147. data/lib/active_support/messages/rotator.rb +38 -31
  148. data/lib/active_support/messages/serializer_with_fallback.rb +158 -0
  149. data/lib/active_support/multibyte/chars.rb +8 -3
  150. data/lib/active_support/multibyte/unicode.rb +9 -37
  151. data/lib/active_support/notifications/fanout.rb +248 -87
  152. data/lib/active_support/notifications/instrumenter.rb +93 -25
  153. data/lib/active_support/notifications.rb +30 -29
  154. data/lib/active_support/number_helper/number_converter.rb +16 -7
  155. data/lib/active_support/number_helper/number_to_currency_converter.rb +6 -6
  156. data/lib/active_support/number_helper/number_to_delimited_converter.rb +18 -5
  157. data/lib/active_support/number_helper/number_to_human_size_converter.rb +3 -3
  158. data/lib/active_support/number_helper/number_to_phone_converter.rb +1 -0
  159. data/lib/active_support/number_helper.rb +379 -317
  160. data/lib/active_support/option_merger.rb +2 -2
  161. data/lib/active_support/ordered_hash.rb +3 -3
  162. data/lib/active_support/ordered_options.rb +67 -15
  163. data/lib/active_support/parameter_filter.rb +103 -84
  164. data/lib/active_support/proxy_object.rb +8 -3
  165. data/lib/active_support/railtie.rb +25 -20
  166. data/lib/active_support/reloader.rb +12 -4
  167. data/lib/active_support/rescuable.rb +2 -0
  168. data/lib/active_support/secure_compare_rotator.rb +16 -9
  169. data/lib/active_support/string_inquirer.rb +4 -2
  170. data/lib/active_support/subscriber.rb +10 -27
  171. data/lib/active_support/syntax_error_proxy.rb +60 -0
  172. data/lib/active_support/tagged_logging.rb +64 -25
  173. data/lib/active_support/test_case.rb +156 -7
  174. data/lib/active_support/testing/assertions.rb +28 -12
  175. data/lib/active_support/testing/autorun.rb +0 -2
  176. data/lib/active_support/testing/constant_stubbing.rb +54 -0
  177. data/lib/active_support/testing/deprecation.rb +20 -27
  178. data/lib/active_support/testing/error_reporter_assertions.rb +107 -0
  179. data/lib/active_support/testing/isolation.rb +46 -33
  180. data/lib/active_support/testing/method_call_assertions.rb +7 -8
  181. data/lib/active_support/testing/parallelization/server.rb +18 -2
  182. data/lib/active_support/testing/parallelization/worker.rb +2 -2
  183. data/lib/active_support/testing/parallelization.rb +12 -1
  184. data/lib/active_support/testing/parallelize_executor.rb +8 -3
  185. data/lib/active_support/testing/setup_and_teardown.rb +2 -0
  186. data/lib/active_support/testing/stream.rb +1 -1
  187. data/lib/active_support/testing/tests_without_assertions.rb +19 -0
  188. data/lib/active_support/testing/time_helpers.rb +38 -16
  189. data/lib/active_support/time_with_zone.rb +16 -41
  190. data/lib/active_support/values/time_zone.rb +25 -14
  191. data/lib/active_support/version.rb +1 -1
  192. data/lib/active_support/xml_mini/jdom.rb +3 -10
  193. data/lib/active_support/xml_mini/nokogiri.rb +1 -1
  194. data/lib/active_support/xml_mini/nokogirisax.rb +1 -1
  195. data/lib/active_support/xml_mini/rexml.rb +1 -1
  196. data/lib/active_support/xml_mini.rb +14 -3
  197. data/lib/active_support.rb +15 -3
  198. metadata +154 -24
  199. data/lib/active_support/core_ext/array/deprecated_conversions.rb +0 -25
  200. data/lib/active_support/core_ext/date/deprecated_conversions.rb +0 -26
  201. data/lib/active_support/core_ext/date_time/deprecated_conversions.rb +0 -22
  202. data/lib/active_support/core_ext/numeric/deprecated_conversions.rb +0 -60
  203. data/lib/active_support/core_ext/range/deprecated_conversions.rb +0 -26
  204. data/lib/active_support/core_ext/range/include_time_with_zone.rb +0 -7
  205. data/lib/active_support/core_ext/range/overlaps.rb +0 -10
  206. data/lib/active_support/core_ext/time/deprecated_conversions.rb +0 -22
  207. data/lib/active_support/core_ext/uri.rb +0 -5
  208. data/lib/active_support/deprecation/instance_delegator.rb +0 -38
  209. data/lib/active_support/per_thread_registry.rb +0 -65
  210. 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>:
229
+ #
230
+ # cache.write("baz", 5, raw: true)
231
+ # cache.increment("baz") # => 6
232
232
  #
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.
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.
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
+ # 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>:
256
+ #
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,36 +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
- case redis
317
- when Redis::Distributed
318
- @mget_capable = true
319
- @mset_capable = false
320
- else
321
- @mget_capable = true
322
- @mset_capable = true
323
- end
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
309
+ else
310
+ c.pipelined { |pipe| yield(pipe, entries) }
311
+ end
312
+ }
324
313
  end
325
314
 
326
315
  # Store provider interface:
@@ -331,35 +320,29 @@ module ActiveSupport
331
320
 
332
321
  def read_serialized_entry(key, raw: false, **options)
333
322
  failsafe :read_entry do
334
- redis.with { |c| c.get(key) }
323
+ redis.then { |c| c.get(key) }
335
324
  end
336
325
  end
337
326
 
338
327
  def read_multi_entries(names, **options)
339
- if mget_capable?
340
- read_multi_mget(*names, **options)
341
- else
342
- super
343
- end
344
- end
345
-
346
- def read_multi_mget(*names)
347
- options = names.extract_options!
348
328
  options = merged_options(options)
349
329
  return {} if names == []
350
330
  raw = options&.fetch(:raw, false)
351
331
 
352
332
  keys = names.map { |name| normalize_key(name, options) }
353
333
 
354
- values = failsafe(:read_multi_mget, returning: {}) do
355
- redis.with { |c| c.mget(*keys) }
334
+ values = failsafe(:read_multi_entries, returning: {}) do
335
+ redis.then { |c| c.mget(*keys) }
356
336
  end
357
337
 
358
338
  names.zip(values).each_with_object({}) do |(name, value), results|
359
339
  if value
360
340
  entry = deserialize_entry(value, raw: raw)
361
341
  unless entry.nil? || entry.expired? || entry.mismatched?(normalize_version(name, options))
362
- results[name] = entry.value
342
+ begin
343
+ results[name] = entry.value
344
+ rescue DeserializationError
345
+ end
363
346
  end
364
347
  end
365
348
  end
@@ -372,7 +355,7 @@ module ActiveSupport
372
355
  write_serialized_entry(key, serialize_entry(entry, raw: raw, **options), raw: raw, **options)
373
356
  end
374
357
 
375
- 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)
376
359
  # If race condition TTL is in use, ensure that cache entries
377
360
  # stick around a bit longer after they would have expired
378
361
  # so we can purposefully serve stale entries.
@@ -386,41 +369,40 @@ module ActiveSupport
386
369
  modifiers[:px] = (1000 * expires_in.to_f).ceil if expires_in
387
370
  end
388
371
 
389
- failsafe :write_entry, returning: false do
390
- redis.with { |c| c.set key, payload, **modifiers }
391
- end
392
- end
393
-
394
- def write_key_expiry(client, key, options)
395
- if options[:expires_in] && client.ttl(key).negative?
396
- 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
397
378
  end
398
379
  end
399
380
 
400
381
  # Delete an entry from the cache.
401
- def delete_entry(key, options)
382
+ def delete_entry(key, **options)
402
383
  failsafe :delete_entry, returning: false do
403
- redis.with { |c| c.del key }
384
+ redis.then { |c| c.del(key) == 1 }
404
385
  end
405
386
  end
406
387
 
407
388
  # Deletes multiple entries in the cache. Returns the number of entries deleted.
408
389
  def delete_multi_entries(entries, **_options)
409
- redis.with { |c| c.del(entries) }
390
+ failsafe :delete_multi_entries, returning: 0 do
391
+ redis.then { |c| c.del(entries) }
392
+ end
410
393
  end
411
394
 
412
395
  # Nonstandard store provider API to write multiple values at once.
413
- def write_multi_entries(entries, expires_in: nil, **options)
414
- if entries.any?
415
- if mset_capable? && expires_in.nil?
416
- failsafe :write_multi_entries do
417
- payload = serialize_entries(entries, **options)
418
- redis.with do |c|
419
- c.mapped_mset(payload)
420
- 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
421
405
  end
422
- else
423
- super
424
406
  end
425
407
  end
426
408
  end
@@ -462,10 +444,43 @@ module ActiveSupport
462
444
  end
463
445
  end
464
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
+
465
481
  def failsafe(method, returning: nil)
466
482
  yield
467
- rescue ::Redis::BaseError => error
468
- ActiveSupport.error_reporter&.report(error, handled: true, severity: :warning)
483
+ rescue ::Redis::BaseError, ConnectionPool::Error, ConnectionPool::TimeoutError => error
469
484
  @error_handler&.call(method: method, exception: error, returning: returning)
470
485
  returning
471
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