activesupport 7.0.0 → 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 (211) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +156 -255
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +6 -6
  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 +41 -9
  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 +111 -129
  15. data/lib/active_support/cache/memory_store.rb +81 -26
  16. data/lib/active_support/cache/null_store.rb +6 -0
  17. data/lib/active_support/cache/redis_cache_store.rb +175 -154
  18. data/lib/active_support/cache/serializer_with_fallback.rb +152 -0
  19. data/lib/active_support/cache/strategy/local_cache.rb +31 -13
  20. data/lib/active_support/cache.rb +457 -377
  21. data/lib/active_support/callbacks.rb +123 -139
  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 +12 -2
  27. data/lib/active_support/core_ext/array/conversions.rb +7 -9
  28. data/lib/active_support/core_ext/array/inquiry.rb +2 -2
  29. data/lib/active_support/core_ext/array.rb +0 -1
  30. data/lib/active_support/core_ext/class/subclasses.rb +4 -15
  31. data/lib/active_support/core_ext/date/blank.rb +4 -0
  32. data/lib/active_support/core_ext/date/calculations.rb +20 -5
  33. data/lib/active_support/core_ext/date/conversions.rb +15 -16
  34. data/lib/active_support/core_ext/date.rb +0 -1
  35. data/lib/active_support/core_ext/date_and_time/calculations.rb +14 -4
  36. data/lib/active_support/core_ext/date_and_time/compatibility.rb +29 -2
  37. data/lib/active_support/core_ext/date_time/blank.rb +4 -0
  38. data/lib/active_support/core_ext/date_time/calculations.rb +4 -0
  39. data/lib/active_support/core_ext/date_time/conversions.rb +15 -15
  40. data/lib/active_support/core_ext/date_time.rb +0 -1
  41. data/lib/active_support/core_ext/digest/uuid.rb +7 -10
  42. data/lib/active_support/core_ext/enumerable.rb +51 -101
  43. data/lib/active_support/core_ext/erb/util.rb +201 -0
  44. data/lib/active_support/core_ext/file/atomic.rb +2 -0
  45. data/lib/active_support/core_ext/hash/conversions.rb +1 -2
  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/indifferent_access.rb +3 -3
  49. data/lib/active_support/core_ext/hash/keys.rb +7 -7
  50. data/lib/active_support/core_ext/integer/inflections.rb +12 -12
  51. data/lib/active_support/core_ext/kernel/singleton_class.rb +1 -1
  52. data/lib/active_support/core_ext/module/attr_internal.rb +17 -6
  53. data/lib/active_support/core_ext/module/attribute_accessors.rb +6 -0
  54. data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +38 -20
  55. data/lib/active_support/core_ext/module/concerning.rb +6 -6
  56. data/lib/active_support/core_ext/module/delegation.rb +20 -119
  57. data/lib/active_support/core_ext/module/deprecation.rb +12 -12
  58. data/lib/active_support/core_ext/module/introspection.rb +0 -1
  59. data/lib/active_support/core_ext/numeric/bytes.rb +9 -0
  60. data/lib/active_support/core_ext/numeric/conversions.rb +77 -75
  61. data/lib/active_support/core_ext/numeric.rb +0 -1
  62. data/lib/active_support/core_ext/object/acts_like.rb +29 -5
  63. data/lib/active_support/core_ext/object/blank.rb +45 -1
  64. data/lib/active_support/core_ext/object/deep_dup.rb +16 -0
  65. data/lib/active_support/core_ext/object/duplicable.rb +25 -16
  66. data/lib/active_support/core_ext/object/inclusion.rb +13 -5
  67. data/lib/active_support/core_ext/object/instance_variables.rb +4 -2
  68. data/lib/active_support/core_ext/object/json.rb +17 -7
  69. data/lib/active_support/core_ext/object/to_query.rb +0 -2
  70. data/lib/active_support/core_ext/object/with.rb +46 -0
  71. data/lib/active_support/core_ext/object/with_options.rb +9 -9
  72. data/lib/active_support/core_ext/object.rb +1 -0
  73. data/lib/active_support/core_ext/pathname/blank.rb +20 -0
  74. data/lib/active_support/core_ext/pathname/existence.rb +2 -0
  75. data/lib/active_support/core_ext/pathname.rb +1 -0
  76. data/lib/active_support/core_ext/range/conversions.rb +32 -11
  77. data/lib/active_support/core_ext/range/overlap.rb +40 -0
  78. data/lib/active_support/core_ext/range.rb +1 -2
  79. data/lib/active_support/core_ext/securerandom.rb +2 -6
  80. data/lib/active_support/core_ext/string/conversions.rb +3 -3
  81. data/lib/active_support/core_ext/string/filters.rb +21 -15
  82. data/lib/active_support/core_ext/string/indent.rb +1 -1
  83. data/lib/active_support/core_ext/string/inflections.rb +16 -9
  84. data/lib/active_support/core_ext/string/inquiry.rb +1 -1
  85. data/lib/active_support/core_ext/string/multibyte.rb +1 -1
  86. data/lib/active_support/core_ext/string/output_safety.rb +39 -150
  87. data/lib/active_support/core_ext/thread/backtrace/location.rb +12 -0
  88. data/lib/active_support/core_ext/time/calculations.rb +42 -32
  89. data/lib/active_support/core_ext/time/compatibility.rb +16 -0
  90. data/lib/active_support/core_ext/time/conversions.rb +13 -15
  91. data/lib/active_support/core_ext/time/zones.rb +8 -9
  92. data/lib/active_support/core_ext/time.rb +0 -1
  93. data/lib/active_support/core_ext.rb +0 -1
  94. data/lib/active_support/current_attributes.rb +53 -46
  95. data/lib/active_support/deep_mergeable.rb +53 -0
  96. data/lib/active_support/delegation.rb +202 -0
  97. data/lib/active_support/dependencies/autoload.rb +9 -16
  98. data/lib/active_support/deprecation/behaviors.rb +65 -42
  99. data/lib/active_support/deprecation/constant_accessor.rb +47 -25
  100. data/lib/active_support/deprecation/deprecators.rb +104 -0
  101. data/lib/active_support/deprecation/disallowed.rb +6 -8
  102. data/lib/active_support/deprecation/method_wrappers.rb +6 -23
  103. data/lib/active_support/deprecation/proxy_wrappers.rb +34 -22
  104. data/lib/active_support/deprecation/reporting.rb +49 -27
  105. data/lib/active_support/deprecation.rb +39 -9
  106. data/lib/active_support/deprecator.rb +7 -0
  107. data/lib/active_support/descendants_tracker.rb +66 -175
  108. data/lib/active_support/duration/iso8601_parser.rb +2 -2
  109. data/lib/active_support/duration/iso8601_serializer.rb +1 -4
  110. data/lib/active_support/duration.rb +13 -7
  111. data/lib/active_support/encrypted_configuration.rb +63 -10
  112. data/lib/active_support/encrypted_file.rb +29 -13
  113. data/lib/active_support/environment_inquirer.rb +22 -2
  114. data/lib/active_support/error_reporter/test_helper.rb +15 -0
  115. data/lib/active_support/error_reporter.rb +160 -36
  116. data/lib/active_support/evented_file_update_checker.rb +19 -7
  117. data/lib/active_support/execution_wrapper.rb +23 -28
  118. data/lib/active_support/file_update_checker.rb +5 -3
  119. data/lib/active_support/fork_tracker.rb +4 -32
  120. data/lib/active_support/gem_version.rb +4 -4
  121. data/lib/active_support/gzip.rb +2 -0
  122. data/lib/active_support/hash_with_indifferent_access.rb +41 -25
  123. data/lib/active_support/html_safe_translation.rb +19 -6
  124. data/lib/active_support/i18n.rb +1 -1
  125. data/lib/active_support/i18n_railtie.rb +20 -13
  126. data/lib/active_support/inflector/inflections.rb +2 -0
  127. data/lib/active_support/inflector/methods.rb +28 -18
  128. data/lib/active_support/inflector/transliterate.rb +4 -2
  129. data/lib/active_support/isolated_execution_state.rb +39 -19
  130. data/lib/active_support/json/decoding.rb +2 -1
  131. data/lib/active_support/json/encoding.rb +25 -43
  132. data/lib/active_support/key_generator.rb +13 -5
  133. data/lib/active_support/lazy_load_hooks.rb +33 -7
  134. data/lib/active_support/locale/en.yml +2 -0
  135. data/lib/active_support/log_subscriber/test_helper.rb +2 -2
  136. data/lib/active_support/log_subscriber.rb +76 -36
  137. data/lib/active_support/logger.rb +22 -60
  138. data/lib/active_support/logger_thread_safe_level.rb +10 -32
  139. data/lib/active_support/message_encryptor.rb +200 -55
  140. data/lib/active_support/message_encryptors.rb +141 -0
  141. data/lib/active_support/message_pack/cache_serializer.rb +23 -0
  142. data/lib/active_support/message_pack/extensions.rb +305 -0
  143. data/lib/active_support/message_pack/serializer.rb +63 -0
  144. data/lib/active_support/message_pack.rb +50 -0
  145. data/lib/active_support/message_verifier.rb +220 -89
  146. data/lib/active_support/message_verifiers.rb +135 -0
  147. data/lib/active_support/messages/codec.rb +65 -0
  148. data/lib/active_support/messages/metadata.rb +111 -45
  149. data/lib/active_support/messages/rotation_coordinator.rb +93 -0
  150. data/lib/active_support/messages/rotator.rb +34 -32
  151. data/lib/active_support/messages/serializer_with_fallback.rb +158 -0
  152. data/lib/active_support/multibyte/chars.rb +4 -2
  153. data/lib/active_support/multibyte/unicode.rb +9 -37
  154. data/lib/active_support/notifications/fanout.rb +248 -87
  155. data/lib/active_support/notifications/instrumenter.rb +93 -25
  156. data/lib/active_support/notifications.rb +38 -31
  157. data/lib/active_support/number_helper/number_converter.rb +16 -7
  158. data/lib/active_support/number_helper/number_to_currency_converter.rb +6 -6
  159. data/lib/active_support/number_helper/number_to_human_size_converter.rb +3 -3
  160. data/lib/active_support/number_helper/number_to_phone_converter.rb +1 -0
  161. data/lib/active_support/number_helper.rb +379 -317
  162. data/lib/active_support/option_merger.rb +4 -4
  163. data/lib/active_support/ordered_hash.rb +3 -3
  164. data/lib/active_support/ordered_options.rb +68 -16
  165. data/lib/active_support/parameter_filter.rb +103 -84
  166. data/lib/active_support/proxy_object.rb +8 -3
  167. data/lib/active_support/railtie.rb +30 -25
  168. data/lib/active_support/reloader.rb +13 -5
  169. data/lib/active_support/rescuable.rb +12 -10
  170. data/lib/active_support/secure_compare_rotator.rb +17 -10
  171. data/lib/active_support/string_inquirer.rb +4 -2
  172. data/lib/active_support/subscriber.rb +10 -27
  173. data/lib/active_support/syntax_error_proxy.rb +60 -0
  174. data/lib/active_support/tagged_logging.rb +64 -25
  175. data/lib/active_support/test_case.rb +160 -7
  176. data/lib/active_support/testing/assertions.rb +29 -13
  177. data/lib/active_support/testing/autorun.rb +0 -2
  178. data/lib/active_support/testing/constant_stubbing.rb +54 -0
  179. data/lib/active_support/testing/deprecation.rb +20 -27
  180. data/lib/active_support/testing/error_reporter_assertions.rb +107 -0
  181. data/lib/active_support/testing/isolation.rb +46 -33
  182. data/lib/active_support/testing/method_call_assertions.rb +7 -8
  183. data/lib/active_support/testing/parallelization/server.rb +3 -0
  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/strict_warnings.rb +43 -0
  188. data/lib/active_support/testing/tests_without_assertions.rb +19 -0
  189. data/lib/active_support/testing/time_helpers.rb +38 -16
  190. data/lib/active_support/time_with_zone.rb +28 -54
  191. data/lib/active_support/values/time_zone.rb +26 -15
  192. data/lib/active_support/version.rb +1 -1
  193. data/lib/active_support/xml_mini/jdom.rb +3 -10
  194. data/lib/active_support/xml_mini/nokogiri.rb +1 -1
  195. data/lib/active_support/xml_mini/nokogirisax.rb +1 -1
  196. data/lib/active_support/xml_mini/rexml.rb +1 -1
  197. data/lib/active_support/xml_mini.rb +13 -4
  198. data/lib/active_support.rb +15 -3
  199. metadata +142 -21
  200. data/lib/active_support/core_ext/array/deprecated_conversions.rb +0 -25
  201. data/lib/active_support/core_ext/date/deprecated_conversions.rb +0 -26
  202. data/lib/active_support/core_ext/date_time/deprecated_conversions.rb +0 -22
  203. data/lib/active_support/core_ext/numeric/deprecated_conversions.rb +0 -60
  204. data/lib/active_support/core_ext/range/deprecated_conversions.rb +0 -26
  205. data/lib/active_support/core_ext/range/include_time_with_zone.rb +0 -7
  206. data/lib/active_support/core_ext/range/overlaps.rb +0 -10
  207. data/lib/active_support/core_ext/time/deprecated_conversions.rb +0 -22
  208. data/lib/active_support/core_ext/uri.rb +0 -5
  209. data/lib/active_support/deprecation/instance_delegator.rb +0 -38
  210. data/lib/active_support/per_thread_registry.rb +0 -65
  211. data/lib/active_support/ruby_features.rb +0 -7
@@ -5,60 +5,54 @@ begin
5
5
  require "redis"
6
6
  require "redis/distributed"
7
7
  rescue LoadError
8
- warn "The Redis cache store requires the redis gem, version 4.0.1 or later. Please add it to your Gemfile: `gem \"redis\", \"~> 4.0\"`"
8
+ warn "The Redis cache store requires the redis gem, version 4.0.1 or later. Please add it to your Gemfile: `gem \"redis\", \">= 4.0.1\"`"
9
9
  raise
10
10
  end
11
11
 
12
- # Prefer the hiredis driver but don't require it.
13
- begin
14
- require "redis/connection/hiredis"
15
- rescue LoadError
16
- end
17
-
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"
18
16
  require "active_support/digest"
19
17
 
20
18
  module ActiveSupport
21
19
  module Cache
22
- module ConnectionPoolLike
23
- def with
24
- yield self
25
- end
26
- end
27
-
28
- ::Redis.include(ConnectionPoolLike)
29
- ::Redis::Distributed.include(ConnectionPoolLike)
30
-
31
- # Redis cache store.
20
+ # = Redis \Cache \Store
32
21
  #
33
- # Deployment note: Take care to use a *dedicated Redis cache* rather
34
- # than pointing this at your existing Redis server. It won't cope well
35
- # 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.
36
26
  #
37
27
  # Redis cache server setup guide: https://redis.io/topics/lru-cache
38
28
  #
39
- # * Supports vanilla Redis, hiredis, and Redis::Distributed.
40
- # * 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+.
41
31
  # * Fault tolerant. If the Redis server is unavailable, no exceptions are
42
32
  # raised. Cache fetches are all misses and writes are dropped.
43
33
  # * Local cache. Hot in-memory primary cache within block/middleware scope.
44
- # * +read_multi+ and +write_multi+ support for Redis mget/mset. Use Redis::Distributed
45
- # 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.
46
36
  # * +delete_matched+ support for Redis KEYS globs.
47
37
  class RedisCacheStore < Store
48
- # Keys are truncated with the ActiveSupport digest if they exceed 1kB
38
+ # Keys are truncated with the Active Support digest if they exceed 1kB
49
39
  MAX_KEY_BYTESIZE = 1024
50
40
 
51
41
  DEFAULT_REDIS_OPTIONS = {
52
- connect_timeout: 20,
42
+ connect_timeout: 1,
53
43
  read_timeout: 1,
54
44
  write_timeout: 1,
55
- reconnect_attempts: 0,
56
45
  }
57
46
 
58
47
  DEFAULT_ERROR_HANDLER = -> (method:, returning:, exception:) do
59
48
  if logger
60
49
  logger.error { "RedisCacheStore: #{method} failed, returned #{returning.inspect}: #{exception.class}: #{exception.message}" }
61
50
  end
51
+ ActiveSupport.error_reporter&.report(
52
+ exception,
53
+ severity: :warning,
54
+ source: "redis_cache_store.active_support",
55
+ )
62
56
  end
63
57
 
64
58
  # The maximum number of entries to receive per SCAN call.
@@ -92,9 +86,11 @@ module ActiveSupport
92
86
  elsif redis
93
87
  redis
94
88
  elsif urls.size > 1
95
- build_redis_distributed_client urls: urls, **redis_options
89
+ build_redis_distributed_client(urls: urls, **redis_options)
90
+ elsif urls.empty?
91
+ build_redis_client(**redis_options)
96
92
  else
97
- build_redis_client url: urls.first, **redis_options
93
+ build_redis_client(url: urls.first, **redis_options)
98
94
  end
99
95
  end
100
96
 
@@ -105,18 +101,21 @@ module ActiveSupport
105
101
  end
106
102
  end
107
103
 
108
- def build_redis_client(url:, **redis_options)
109
- ::Redis.new DEFAULT_REDIS_OPTIONS.merge(redis_options.merge(url: url))
104
+ def build_redis_client(**redis_options)
105
+ ::Redis.new(DEFAULT_REDIS_OPTIONS.merge(redis_options))
110
106
  end
111
107
  end
112
108
 
113
- attr_reader :redis_options
114
109
  attr_reader :max_key_bytesize
110
+ attr_reader :redis
115
111
 
116
112
  # Creates a new Redis cache store.
117
113
  #
118
- # Handles four options: :redis block, :redis instance, single :url
119
- # 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.
120
119
  #
121
120
  # Option Class Result
122
121
  # :redis Proc -> options[:redis].call
@@ -139,35 +138,31 @@ module ActiveSupport
139
138
  #
140
139
  # Race condition TTL is not set by default. This can be used to avoid
141
140
  # "thundering herd" cache writes when hot cache entries are expired.
142
- # See <tt>ActiveSupport::Cache::Store#fetch</tt> for more.
143
- 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)
144
- @redis_options = redis_options
141
+ # See ActiveSupport::Cache::Store#fetch for more.
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
145
157
 
146
158
  @max_key_bytesize = MAX_KEY_BYTESIZE
147
159
  @error_handler = error_handler
148
160
 
149
- super namespace: namespace,
150
- compress: compress, compress_threshold: compress_threshold,
151
- expires_in: expires_in, race_condition_ttl: race_condition_ttl,
152
- coder: coder
153
- end
154
-
155
- def redis
156
- @redis ||= begin
157
- pool_options = self.class.send(:retrieve_pool_options, redis_options)
158
-
159
- if pool_options.any?
160
- self.class.send(:ensure_connection_pool_added!)
161
- ::ConnectionPool.new(pool_options) { self.class.build_redis(**redis_options) }
162
- else
163
- self.class.build_redis(**redis_options)
164
- end
165
- end
161
+ super(universal_options)
166
162
  end
167
163
 
168
164
  def inspect
169
- instance = @redis || @redis_options
170
- "#<#{self.class} options=#{options.inspect} redis=#{instance.inspect}>"
165
+ "#<#{self.class} options=#{options.inspect} redis=#{redis.inspect}>"
171
166
  end
172
167
 
173
168
  # Cache Store API implementation.
@@ -175,14 +170,13 @@ module ActiveSupport
175
170
  # Read multiple values at once. Returns a hash of requested keys ->
176
171
  # fetched values.
177
172
  def read_multi(*names)
178
- if mget_capable?
179
- instrument(:read_multi, names, options) do |payload|
180
- read_multi_mget(*names).tap do |results|
181
- payload[:hits] = results.keys
182
- 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
183
179
  end
184
- else
185
- super
186
180
  end
187
181
  end
188
182
 
@@ -202,12 +196,13 @@ module ActiveSupport
202
196
  #
203
197
  # Failsafe: Raises errors.
204
198
  def delete_matched(matcher, options = nil)
205
- instrument :delete_matched, matcher do
206
- unless String === matcher
207
- raise ArgumentError, "Only Redis glob strings are supported: #{matcher.inspect}"
208
- end
209
- redis.with do |c|
210
- 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|
211
206
  cursor = "0"
212
207
  # Fetch keys in batches using SCAN to avoid blocking the Redis server.
213
208
  nodes = c.respond_to?(:nodes) ? c.nodes : [c]
@@ -222,48 +217,57 @@ module ActiveSupport
222
217
  end
223
218
  end
224
219
 
225
- # Cache Store API implementation.
220
+ # Increment a cached integer value using the Redis incrby atomic operator.
221
+ # Returns the updated value.
226
222
  #
227
- # Increment a cached value. This method uses the Redis incr atomic
228
- # operator and can only be used on values written with the :raw option.
229
- # Calling it on a value not stored with :raw will initialize that value
230
- # to zero.
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
+ #
233
+ # Incrementing a non-numeric value, or a value written without
234
+ # <tt>raw: true</tt>, will fail and return +nil+.
231
235
  #
232
236
  # Failsafe: Raises errors.
233
237
  def increment(name, amount = 1, options = nil)
234
- instrument :increment, name, amount: amount do
235
- failsafe :increment do
236
- options = merged_options(options)
237
- key = normalize_key(name, options)
238
+ options = merged_options(options)
239
+ key = normalize_key(name, options)
238
240
 
239
- redis.with do |c|
240
- c.incrby(key, amount).tap do
241
- write_key_expiry(c, key, options)
242
- end
243
- end
241
+ instrument :increment, key, amount: amount do
242
+ failsafe :increment do
243
+ change_counter(key, amount, options)
244
244
  end
245
245
  end
246
246
  end
247
247
 
248
- # 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+:
249
252
  #
250
- # Decrement a cached value. This method uses the Redis decr atomic
251
- # operator and can only be used on values written with the :raw option.
252
- # Calling it on a value not stored with :raw will initialize that value
253
- # to zero.
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+.
254
262
  #
255
263
  # Failsafe: Raises errors.
256
264
  def decrement(name, amount = 1, options = nil)
257
- instrument :decrement, name, amount: amount do
258
- failsafe :decrement do
259
- options = merged_options(options)
260
- key = normalize_key(name, options)
265
+ options = merged_options(options)
266
+ key = normalize_key(name, options)
261
267
 
262
- redis.with do |c|
263
- c.decrby(key, amount).tap do
264
- write_key_expiry(c, key, options)
265
- end
266
- end
268
+ instrument :decrement, key, amount: amount do
269
+ failsafe :decrement do
270
+ change_counter(key, -amount, options)
267
271
  end
268
272
  end
269
273
  end
@@ -285,36 +289,27 @@ module ActiveSupport
285
289
  if namespace = merged_options(options)[:namespace]
286
290
  delete_matched "*", namespace: namespace
287
291
  else
288
- redis.with { |c| c.flushdb }
292
+ redis.then { |c| c.flushdb }
289
293
  end
290
294
  end
291
295
  end
292
296
 
293
297
  # Get info from redis servers.
294
298
  def stats
295
- redis.with { |c| c.info }
296
- end
297
-
298
- def mget_capable? # :nodoc:
299
- set_redis_capabilities unless defined? @mget_capable
300
- @mget_capable
301
- end
302
-
303
- def mset_capable? # :nodoc:
304
- set_redis_capabilities unless defined? @mset_capable
305
- @mset_capable
299
+ redis.then { |c| c.info }
306
300
  end
307
301
 
308
302
  private
309
- def set_redis_capabilities
310
- case redis
311
- when Redis::Distributed
312
- @mget_capable = true
313
- @mset_capable = false
314
- else
315
- @mget_capable = true
316
- @mset_capable = true
317
- 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
+ }
318
313
  end
319
314
 
320
315
  # Store provider interface:
@@ -325,35 +320,29 @@ module ActiveSupport
325
320
 
326
321
  def read_serialized_entry(key, raw: false, **options)
327
322
  failsafe :read_entry do
328
- redis.with { |c| c.get(key) }
323
+ redis.then { |c| c.get(key) }
329
324
  end
330
325
  end
331
326
 
332
327
  def read_multi_entries(names, **options)
333
- if mget_capable?
334
- read_multi_mget(*names, **options)
335
- else
336
- super
337
- end
338
- end
339
-
340
- def read_multi_mget(*names)
341
- options = names.extract_options!
342
328
  options = merged_options(options)
343
329
  return {} if names == []
344
330
  raw = options&.fetch(:raw, false)
345
331
 
346
332
  keys = names.map { |name| normalize_key(name, options) }
347
333
 
348
- values = failsafe(:read_multi_mget, returning: {}) do
349
- redis.with { |c| c.mget(*keys) }
334
+ values = failsafe(:read_multi_entries, returning: {}) do
335
+ redis.then { |c| c.mget(*keys) }
350
336
  end
351
337
 
352
338
  names.zip(values).each_with_object({}) do |(name, value), results|
353
339
  if value
354
340
  entry = deserialize_entry(value, raw: raw)
355
341
  unless entry.nil? || entry.expired? || entry.mismatched?(normalize_version(name, options))
356
- results[name] = entry.value
342
+ begin
343
+ results[name] = entry.value
344
+ rescue DeserializationError
345
+ end
357
346
  end
358
347
  end
359
348
  end
@@ -366,7 +355,7 @@ module ActiveSupport
366
355
  write_serialized_entry(key, serialize_entry(entry, raw: raw, **options), raw: raw, **options)
367
356
  end
368
357
 
369
- 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)
370
359
  # If race condition TTL is in use, ensure that cache entries
371
360
  # stick around a bit longer after they would have expired
372
361
  # so we can purposefully serve stale entries.
@@ -380,41 +369,40 @@ module ActiveSupport
380
369
  modifiers[:px] = (1000 * expires_in.to_f).ceil if expires_in
381
370
  end
382
371
 
383
- failsafe :write_entry, returning: false do
384
- redis.with { |c| c.set key, payload, **modifiers }
385
- end
386
- end
387
-
388
- def write_key_expiry(client, key, options)
389
- if options[:expires_in] && client.ttl(key).negative?
390
- 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
391
378
  end
392
379
  end
393
380
 
394
381
  # Delete an entry from the cache.
395
- def delete_entry(key, options)
382
+ def delete_entry(key, **options)
396
383
  failsafe :delete_entry, returning: false do
397
- redis.with { |c| c.del key }
384
+ redis.then { |c| c.del(key) == 1 }
398
385
  end
399
386
  end
400
387
 
401
388
  # Deletes multiple entries in the cache. Returns the number of entries deleted.
402
389
  def delete_multi_entries(entries, **_options)
403
- redis.with { |c| c.del(entries) }
390
+ failsafe :delete_multi_entries, returning: 0 do
391
+ redis.then { |c| c.del(entries) }
392
+ end
404
393
  end
405
394
 
406
395
  # Nonstandard store provider API to write multiple values at once.
407
- def write_multi_entries(entries, expires_in: nil, **options)
408
- if entries.any?
409
- if mset_capable? && expires_in.nil?
410
- failsafe :write_multi_entries do
411
- payload = serialize_entries(entries, **options)
412
- redis.with do |c|
413
- c.mapped_mset(payload)
414
- 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
415
405
  end
416
- else
417
- super
418
406
  end
419
407
  end
420
408
  end
@@ -456,10 +444,43 @@ module ActiveSupport
456
444
  end
457
445
  end
458
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
+
459
481
  def failsafe(method, returning: nil)
460
482
  yield
461
483
  rescue ::Redis::BaseError => error
462
- ActiveSupport.error_reporter&.report(error, handled: true, severity: :warning)
463
484
  @error_handler&.call(method: method, exception: error, returning: returning)
464
485
  returning
465
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