activesupport 7.0.4 → 7.1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1076 -230
  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 +2 -0
  7. data/lib/active_support/backtrace_cleaner.rb +30 -5
  8. data/lib/active_support/benchmarkable.rb +1 -0
  9. data/lib/active_support/broadcast_logger.rb +251 -0
  10. data/lib/active_support/builder.rb +1 -1
  11. data/lib/active_support/cache/coder.rb +153 -0
  12. data/lib/active_support/cache/entry.rb +134 -0
  13. data/lib/active_support/cache/file_store.rb +37 -10
  14. data/lib/active_support/cache/mem_cache_store.rb +100 -76
  15. data/lib/active_support/cache/memory_store.rb +78 -24
  16. data/lib/active_support/cache/null_store.rb +6 -0
  17. data/lib/active_support/cache/redis_cache_store.rb +153 -141
  18. data/lib/active_support/cache/serializer_with_fallback.rb +175 -0
  19. data/lib/active_support/cache/strategy/local_cache.rb +29 -14
  20. data/lib/active_support/cache.rb +333 -253
  21. data/lib/active_support/callbacks.rb +44 -21
  22. data/lib/active_support/code_generator.rb +15 -10
  23. data/lib/active_support/concern.rb +4 -2
  24. data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +42 -3
  25. data/lib/active_support/concurrency/null_lock.rb +13 -0
  26. data/lib/active_support/configurable.rb +10 -0
  27. data/lib/active_support/core_ext/array/conversions.rb +2 -1
  28. data/lib/active_support/core_ext/array.rb +0 -1
  29. data/lib/active_support/core_ext/class/subclasses.rb +13 -10
  30. data/lib/active_support/core_ext/date/calculations.rb +15 -0
  31. data/lib/active_support/core_ext/date/conversions.rb +2 -1
  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_time/calculations.rb +4 -0
  35. data/lib/active_support/core_ext/date_time/conversions.rb +6 -2
  36. data/lib/active_support/core_ext/date_time.rb +0 -1
  37. data/lib/active_support/core_ext/digest/uuid.rb +1 -10
  38. data/lib/active_support/core_ext/enumerable.rb +8 -75
  39. data/lib/active_support/core_ext/erb/util.rb +196 -0
  40. data/lib/active_support/core_ext/hash/conversions.rb +1 -1
  41. data/lib/active_support/core_ext/hash/deep_merge.rb +22 -14
  42. data/lib/active_support/core_ext/hash/deep_transform_values.rb +3 -3
  43. data/lib/active_support/core_ext/hash/keys.rb +3 -3
  44. data/lib/active_support/core_ext/integer/inflections.rb +12 -12
  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 +81 -37
  49. data/lib/active_support/core_ext/module/deprecation.rb +15 -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 +2 -0
  53. data/lib/active_support/core_ext/numeric.rb +0 -1
  54. data/lib/active_support/core_ext/object/deep_dup.rb +16 -0
  55. data/lib/active_support/core_ext/object/duplicable.rb +25 -16
  56. data/lib/active_support/core_ext/object/inclusion.rb +13 -5
  57. data/lib/active_support/core_ext/object/instance_variables.rb +22 -12
  58. data/lib/active_support/core_ext/object/json.rb +16 -6
  59. data/lib/active_support/core_ext/object/to_query.rb +0 -2
  60. data/lib/active_support/core_ext/object/with.rb +44 -0
  61. data/lib/active_support/core_ext/object/with_options.rb +9 -9
  62. data/lib/active_support/core_ext/object.rb +1 -0
  63. data/lib/active_support/core_ext/pathname/blank.rb +16 -0
  64. data/lib/active_support/core_ext/pathname/existence.rb +2 -0
  65. data/lib/active_support/core_ext/pathname.rb +1 -0
  66. data/lib/active_support/core_ext/range/conversions.rb +28 -7
  67. data/lib/active_support/core_ext/range/overlap.rb +40 -0
  68. data/lib/active_support/core_ext/range.rb +1 -2
  69. data/lib/active_support/core_ext/securerandom.rb +24 -12
  70. data/lib/active_support/core_ext/string/filters.rb +20 -14
  71. data/lib/active_support/core_ext/string/indent.rb +1 -1
  72. data/lib/active_support/core_ext/string/inflections.rb +16 -9
  73. data/lib/active_support/core_ext/string/output_safety.rb +42 -174
  74. data/lib/active_support/core_ext/thread/backtrace/location.rb +12 -0
  75. data/lib/active_support/core_ext/time/calculations.rb +22 -2
  76. data/lib/active_support/core_ext/time/conversions.rb +2 -2
  77. data/lib/active_support/core_ext/time/zones.rb +7 -8
  78. data/lib/active_support/core_ext/time.rb +0 -1
  79. data/lib/active_support/current_attributes.rb +15 -6
  80. data/lib/active_support/deep_mergeable.rb +53 -0
  81. data/lib/active_support/dependencies/autoload.rb +17 -12
  82. data/lib/active_support/deprecation/behaviors.rb +65 -42
  83. data/lib/active_support/deprecation/constant_accessor.rb +5 -4
  84. data/lib/active_support/deprecation/deprecators.rb +104 -0
  85. data/lib/active_support/deprecation/disallowed.rb +6 -8
  86. data/lib/active_support/deprecation/instance_delegator.rb +31 -4
  87. data/lib/active_support/deprecation/method_wrappers.rb +6 -23
  88. data/lib/active_support/deprecation/proxy_wrappers.rb +37 -22
  89. data/lib/active_support/deprecation/reporting.rb +43 -26
  90. data/lib/active_support/deprecation.rb +32 -5
  91. data/lib/active_support/deprecator.rb +7 -0
  92. data/lib/active_support/descendants_tracker.rb +104 -132
  93. data/lib/active_support/duration/iso8601_serializer.rb +0 -2
  94. data/lib/active_support/duration.rb +2 -1
  95. data/lib/active_support/encrypted_configuration.rb +63 -11
  96. data/lib/active_support/encrypted_file.rb +16 -12
  97. data/lib/active_support/environment_inquirer.rb +22 -2
  98. data/lib/active_support/error_reporter/test_helper.rb +15 -0
  99. data/lib/active_support/error_reporter.rb +121 -35
  100. data/lib/active_support/evented_file_update_checker.rb +17 -2
  101. data/lib/active_support/execution_wrapper.rb +4 -4
  102. data/lib/active_support/file_update_checker.rb +4 -2
  103. data/lib/active_support/fork_tracker.rb +10 -2
  104. data/lib/active_support/gem_version.rb +4 -4
  105. data/lib/active_support/gzip.rb +2 -0
  106. data/lib/active_support/hash_with_indifferent_access.rb +35 -17
  107. data/lib/active_support/html_safe_translation.rb +16 -6
  108. data/lib/active_support/i18n.rb +1 -1
  109. data/lib/active_support/i18n_railtie.rb +20 -13
  110. data/lib/active_support/inflector/inflections.rb +2 -0
  111. data/lib/active_support/inflector/methods.rb +28 -18
  112. data/lib/active_support/inflector/transliterate.rb +3 -1
  113. data/lib/active_support/isolated_execution_state.rb +26 -22
  114. data/lib/active_support/json/decoding.rb +2 -1
  115. data/lib/active_support/json/encoding.rb +25 -43
  116. data/lib/active_support/key_generator.rb +9 -1
  117. data/lib/active_support/lazy_load_hooks.rb +7 -5
  118. data/lib/active_support/locale/en.yml +2 -0
  119. data/lib/active_support/log_subscriber.rb +85 -33
  120. data/lib/active_support/logger.rb +9 -60
  121. data/lib/active_support/logger_thread_safe_level.rb +10 -24
  122. data/lib/active_support/message_encryptor.rb +197 -53
  123. data/lib/active_support/message_encryptors.rb +141 -0
  124. data/lib/active_support/message_pack/cache_serializer.rb +23 -0
  125. data/lib/active_support/message_pack/extensions.rb +292 -0
  126. data/lib/active_support/message_pack/serializer.rb +63 -0
  127. data/lib/active_support/message_pack.rb +50 -0
  128. data/lib/active_support/message_verifier.rb +212 -93
  129. data/lib/active_support/message_verifiers.rb +135 -0
  130. data/lib/active_support/messages/codec.rb +65 -0
  131. data/lib/active_support/messages/metadata.rb +111 -45
  132. data/lib/active_support/messages/rotation_coordinator.rb +93 -0
  133. data/lib/active_support/messages/rotator.rb +34 -32
  134. data/lib/active_support/messages/serializer_with_fallback.rb +158 -0
  135. data/lib/active_support/multibyte/chars.rb +2 -0
  136. data/lib/active_support/multibyte/unicode.rb +9 -37
  137. data/lib/active_support/notifications/fanout.rb +245 -81
  138. data/lib/active_support/notifications/instrumenter.rb +87 -22
  139. data/lib/active_support/notifications.rb +3 -3
  140. data/lib/active_support/number_helper/number_converter.rb +14 -5
  141. data/lib/active_support/number_helper/number_to_currency_converter.rb +6 -6
  142. data/lib/active_support/number_helper/number_to_human_size_converter.rb +3 -3
  143. data/lib/active_support/number_helper/number_to_phone_converter.rb +1 -0
  144. data/lib/active_support/number_helper.rb +379 -317
  145. data/lib/active_support/ordered_hash.rb +3 -3
  146. data/lib/active_support/ordered_options.rb +14 -0
  147. data/lib/active_support/parameter_filter.rb +103 -84
  148. data/lib/active_support/proxy_object.rb +2 -0
  149. data/lib/active_support/railtie.rb +33 -21
  150. data/lib/active_support/reloader.rb +12 -4
  151. data/lib/active_support/rescuable.rb +2 -0
  152. data/lib/active_support/secure_compare_rotator.rb +16 -9
  153. data/lib/active_support/string_inquirer.rb +3 -1
  154. data/lib/active_support/subscriber.rb +9 -27
  155. data/lib/active_support/syntax_error_proxy.rb +60 -0
  156. data/lib/active_support/tagged_logging.rb +64 -24
  157. data/lib/active_support/test_case.rb +153 -6
  158. data/lib/active_support/testing/assertions.rb +26 -10
  159. data/lib/active_support/testing/autorun.rb +0 -2
  160. data/lib/active_support/testing/constant_stubbing.rb +32 -0
  161. data/lib/active_support/testing/deprecation.rb +25 -25
  162. data/lib/active_support/testing/error_reporter_assertions.rb +107 -0
  163. data/lib/active_support/testing/isolation.rb +29 -28
  164. data/lib/active_support/testing/method_call_assertions.rb +21 -8
  165. data/lib/active_support/testing/parallelize_executor.rb +8 -3
  166. data/lib/active_support/testing/setup_and_teardown.rb +2 -0
  167. data/lib/active_support/testing/stream.rb +1 -1
  168. data/lib/active_support/testing/strict_warnings.rb +39 -0
  169. data/lib/active_support/testing/time_helpers.rb +37 -15
  170. data/lib/active_support/time_with_zone.rb +8 -37
  171. data/lib/active_support/values/time_zone.rb +18 -7
  172. data/lib/active_support/version.rb +1 -1
  173. data/lib/active_support/xml_mini/jdom.rb +3 -10
  174. data/lib/active_support/xml_mini/nokogiri.rb +1 -1
  175. data/lib/active_support/xml_mini/nokogirisax.rb +1 -1
  176. data/lib/active_support/xml_mini/rexml.rb +1 -1
  177. data/lib/active_support/xml_mini.rb +2 -2
  178. data/lib/active_support.rb +14 -3
  179. metadata +148 -19
  180. data/lib/active_support/core_ext/array/deprecated_conversions.rb +0 -25
  181. data/lib/active_support/core_ext/date/deprecated_conversions.rb +0 -26
  182. data/lib/active_support/core_ext/date_time/deprecated_conversions.rb +0 -22
  183. data/lib/active_support/core_ext/numeric/deprecated_conversions.rb +0 -60
  184. data/lib/active_support/core_ext/range/deprecated_conversions.rb +0 -26
  185. data/lib/active_support/core_ext/range/include_time_with_zone.rb +0 -7
  186. data/lib/active_support/core_ext/range/overlaps.rb +0 -10
  187. data/lib/active_support/core_ext/time/deprecated_conversions.rb +0 -22
  188. data/lib/active_support/core_ext/uri.rb +0 -5
  189. data/lib/active_support/per_thread_registry.rb +0 -65
@@ -4,10 +4,12 @@ require "monitor"
4
4
 
5
5
  module ActiveSupport
6
6
  module Cache
7
+ # = Memory \Cache \Store
8
+ #
7
9
  # A cache store implementation which stores everything into memory in the
8
- # same process. If you're running multiple Ruby on Rails server processes
10
+ # same process. If you're running multiple Ruby on \Rails server processes
9
11
  # (which is the case if you're using Phusion Passenger or puma clustered mode),
10
- # then this means that Rails server process instances won't be able
12
+ # then this means that \Rails server process instances won't be able
11
13
  # to share cache data with each other and this may not be the most
12
14
  # appropriate cache in that scenario.
13
15
  #
@@ -16,37 +18,61 @@ module ActiveSupport
16
18
  # a cleanup will occur which tries to prune the cache down to three quarters
17
19
  # of the maximum size by removing the least recently used entries.
18
20
  #
19
- # Unlike other Cache store implementations, MemoryStore does not compress
20
- # values by default. MemoryStore does not benefit from compression as much
21
+ # Unlike other Cache store implementations, +MemoryStore+ does not compress
22
+ # values by default. +MemoryStore+ does not benefit from compression as much
21
23
  # as other Store implementations, as it does not send data over a network.
22
24
  # However, when compression is enabled, it still pays the full cost of
23
25
  # compression in terms of cpu use.
24
26
  #
25
- # MemoryStore is thread-safe.
27
+ # +MemoryStore+ is thread-safe.
26
28
  class MemoryStore < Store
27
29
  module DupCoder # :nodoc:
28
30
  extend self
29
31
 
30
32
  def dump(entry)
31
- entry.dup_value! unless entry.compressed?
32
- entry
33
+ if entry.value && entry.value != true && !entry.value.is_a?(Numeric)
34
+ Cache::Entry.new(dump_value(entry.value), expires_at: entry.expires_at, version: entry.version)
35
+ else
36
+ entry
37
+ end
33
38
  end
34
39
 
35
40
  def dump_compressed(entry, threshold)
36
- entry = entry.compressed(threshold)
37
- entry.dup_value! unless entry.compressed?
38
- entry
41
+ compressed_entry = entry.compressed(threshold)
42
+ compressed_entry.compressed? ? compressed_entry : dump(entry)
39
43
  end
40
44
 
41
45
  def load(entry)
42
- entry = entry.dup
43
- entry.dup_value!
44
- entry
46
+ if !entry.compressed? && entry.value.is_a?(String)
47
+ Cache::Entry.new(load_value(entry.value), expires_at: entry.expires_at, version: entry.version)
48
+ else
49
+ entry
50
+ end
45
51
  end
52
+
53
+ private
54
+ MARSHAL_SIGNATURE = "\x04\x08".b.freeze
55
+
56
+ def dump_value(value)
57
+ if value.is_a?(String) && !value.start_with?(MARSHAL_SIGNATURE)
58
+ value.dup
59
+ else
60
+ Marshal.dump(value)
61
+ end
62
+ end
63
+
64
+ def load_value(string)
65
+ if string.start_with?(MARSHAL_SIGNATURE)
66
+ Marshal.load(string)
67
+ else
68
+ string.dup
69
+ end
70
+ end
46
71
  end
47
72
 
48
73
  def initialize(options = nil)
49
74
  options ||= {}
75
+ options[:coder] = DupCoder unless options.key?(:coder) || options.key?(:serializer)
50
76
  # Disable compression by default.
51
77
  options[:compress] ||= false
52
78
  super(options)
@@ -74,7 +100,7 @@ module ActiveSupport
74
100
  # Preemptively iterates through all stored keys and removes the ones which have expired.
75
101
  def cleanup(options = nil)
76
102
  options = merged_options(options)
77
- instrument(:cleanup, size: @data.size) do
103
+ _instrument(:cleanup, size: @data.size) do
78
104
  keys = synchronize { @data.keys }
79
105
  keys.each do |key|
80
106
  entry = @data[key]
@@ -108,12 +134,33 @@ module ActiveSupport
108
134
  @pruning
109
135
  end
110
136
 
111
- # Increment an integer value in the cache.
137
+ # Increment a cached integer value. Returns the updated value.
138
+ #
139
+ # If the key is unset, it will be set to +amount+:
140
+ #
141
+ # cache.increment("foo") # => 1
142
+ # cache.increment("bar", 100) # => 100
143
+ #
144
+ # To set a specific value, call #write:
145
+ #
146
+ # cache.write("baz", 5)
147
+ # cache.increment("baz") # => 6
148
+ #
112
149
  def increment(name, amount = 1, options = nil)
113
150
  modify_value(name, amount, options)
114
151
  end
115
152
 
116
- # Decrement an integer value in the cache.
153
+ # Decrement a cached integer value. Returns the updated value.
154
+ #
155
+ # If the key is unset or has expired, it will be set to +-amount+.
156
+ #
157
+ # cache.decrement("foo") # => -1
158
+ #
159
+ # To set a specific value, call #write:
160
+ #
161
+ # cache.write("baz", 5)
162
+ # cache.decrement("baz") # => 4
163
+ #
117
164
  def decrement(name, amount = 1, options = nil)
118
165
  modify_value(name, -amount, options)
119
166
  end
@@ -143,10 +190,6 @@ module ActiveSupport
143
190
  private
144
191
  PER_ENTRY_OVERHEAD = 240
145
192
 
146
- def default_coder
147
- DupCoder
148
- end
149
-
150
193
  def cached_size(key, payload)
151
194
  key.to_s.bytesize + payload.bytesize + PER_ENTRY_OVERHEAD
152
195
  end
@@ -166,7 +209,7 @@ module ActiveSupport
166
209
  def write_entry(key, entry, **options)
167
210
  payload = serialize_entry(entry, **options)
168
211
  synchronize do
169
- return false if options[:unless_exist] && @data.key?(key)
212
+ return false if options[:unless_exist] && exist?(key, namespace: nil)
170
213
 
171
214
  old_payload = @data[key]
172
215
  if old_payload
@@ -188,12 +231,23 @@ module ActiveSupport
188
231
  end
189
232
  end
190
233
 
234
+ # Modifies the amount of an integer value that is stored in the cache.
235
+ # If the key is not found it is created and set to +amount+.
191
236
  def modify_value(name, amount, options)
192
237
  options = merged_options(options)
238
+ key = normalize_key(name, options)
239
+ version = normalize_version(name, options)
240
+
193
241
  synchronize do
194
- if num = read(name, options)
195
- num = num.to_i + amount
196
- write(name, num, options)
242
+ entry = read_entry(key, **options)
243
+
244
+ if !entry || entry.expired? || entry.mismatched?(version)
245
+ write(name, Integer(amount), options)
246
+ amount
247
+ else
248
+ num = entry.value.to_i + amount
249
+ entry = Entry.new(num, expires_at: entry.expires_at, version: entry.version)
250
+ write_entry(key, entry)
197
251
  num
198
252
  end
199
253
  end
@@ -2,6 +2,8 @@
2
2
 
3
3
  module ActiveSupport
4
4
  module Cache
5
+ # = Null \Cache \Store
6
+ #
5
7
  # A cache store implementation which doesn't actually store anything. Useful in
6
8
  # development and test environments where you don't want caching turned on but
7
9
  # need to go through the caching interface.
@@ -32,6 +34,10 @@ module ActiveSupport
32
34
  def delete_matched(matcher, options = nil)
33
35
  end
34
36
 
37
+ def inspect # :nodoc:
38
+ "#<#{self.class.name} options=#{@options.inspect}>"
39
+ end
40
+
35
41
  private
36
42
  def read_entry(key, **s)
37
43
  deserialize_entry(read_serialized_entry(key))
@@ -9,60 +9,50 @@ rescue LoadError
9
9
  raise
10
10
  end
11
11
 
12
- # Prefer the hiredis driver but don't require it.
13
- begin
14
- if ::Redis::VERSION < "5"
15
- require "redis/connection/hiredis"
16
- else
17
- require "hiredis-client"
18
- end
19
- rescue LoadError
20
- end
21
-
12
+ require "connection_pool"
13
+ require "active_support/core_ext/array/wrap"
14
+ require "active_support/core_ext/hash/slice"
15
+ require "active_support/core_ext/numeric/time"
22
16
  require "active_support/digest"
23
17
 
24
18
  module ActiveSupport
25
19
  module Cache
26
- module ConnectionPoolLike
27
- def with
28
- yield self
29
- end
30
- end
31
-
32
- ::Redis.include(ConnectionPoolLike)
33
- ::Redis::Distributed.include(ConnectionPoolLike)
34
-
35
- # Redis cache store.
20
+ # = Redis \Cache \Store
36
21
  #
37
- # Deployment note: Take care to use a *dedicated Redis cache* rather
38
- # than pointing this at your existing Redis server. It won't cope well
39
- # with mixed usage patterns and it won't expire cache entries by default.
22
+ # Deployment note: Take care to use a <b>dedicated Redis cache</b> rather
23
+ # than pointing this at a persistent Redis server (for example, one used as
24
+ # an Active Job queue). Redis won't cope well with mixed usage patterns and it
25
+ # won't expire cache entries by default.
40
26
  #
41
27
  # Redis cache server setup guide: https://redis.io/topics/lru-cache
42
28
  #
43
- # * Supports vanilla Redis, hiredis, and Redis::Distributed.
44
- # * Supports Memcached-like sharding across Redises with Redis::Distributed.
29
+ # * Supports vanilla Redis, hiredis, and +Redis::Distributed+.
30
+ # * Supports Memcached-like sharding across Redises with +Redis::Distributed+.
45
31
  # * Fault tolerant. If the Redis server is unavailable, no exceptions are
46
32
  # raised. Cache fetches are all misses and writes are dropped.
47
33
  # * Local cache. Hot in-memory primary cache within block/middleware scope.
48
- # * +read_multi+ and +write_multi+ support for Redis mget/mset. Use Redis::Distributed
49
- # 4.0.1+ for distributed mget support.
34
+ # * +read_multi+ and +write_multi+ support for Redis mget/mset. Use
35
+ # +Redis::Distributed+ 4.0.1+ for distributed mget support.
50
36
  # * +delete_matched+ support for Redis KEYS globs.
51
37
  class RedisCacheStore < Store
52
- # Keys are truncated with the ActiveSupport digest if they exceed 1kB
38
+ # Keys are truncated with the Active Support digest if they exceed 1kB
53
39
  MAX_KEY_BYTESIZE = 1024
54
40
 
55
41
  DEFAULT_REDIS_OPTIONS = {
56
- connect_timeout: 20,
42
+ connect_timeout: 1,
57
43
  read_timeout: 1,
58
44
  write_timeout: 1,
59
- reconnect_attempts: 0,
60
45
  }
61
46
 
62
47
  DEFAULT_ERROR_HANDLER = -> (method:, returning:, exception:) do
63
48
  if logger
64
49
  logger.error { "RedisCacheStore: #{method} failed, returned #{returning.inspect}: #{exception.class}: #{exception.message}" }
65
50
  end
51
+ ActiveSupport.error_reporter&.report(
52
+ exception,
53
+ severity: :warning,
54
+ source: "redis_cache_store.active_support",
55
+ )
66
56
  end
67
57
 
68
58
  # The maximum number of entries to receive per SCAN call.
@@ -116,13 +106,16 @@ module ActiveSupport
116
106
  end
117
107
  end
118
108
 
119
- attr_reader :redis_options
120
109
  attr_reader :max_key_bytesize
110
+ attr_reader :redis
121
111
 
122
112
  # Creates a new Redis cache store.
123
113
  #
124
- # Handles four options: :redis block, :redis instance, single :url
125
- # string, and multiple :url strings.
114
+ # There are four ways to provide the Redis client used by the cache: the
115
+ # +:redis+ param can be a Redis instance or a block that returns a Redis
116
+ # instance, or the +:url+ param can be a string or an array of strings
117
+ # which will be used to create a Redis instance or a +Redis::Distributed+
118
+ # instance.
126
119
  #
127
120
  # Option Class Result
128
121
  # :redis Proc -> options[:redis].call
@@ -146,34 +139,30 @@ module ActiveSupport
146
139
  # Race condition TTL is not set by default. This can be used to avoid
147
140
  # "thundering herd" cache writes when hot cache entries are expired.
148
141
  # See ActiveSupport::Cache::Store#fetch for more.
149
- def initialize(namespace: nil, compress: true, compress_threshold: 1.kilobyte, coder: default_coder, expires_in: nil, race_condition_ttl: nil, error_handler: DEFAULT_ERROR_HANDLER, **redis_options)
150
- @redis_options = redis_options
142
+ #
143
+ # Setting <tt>skip_nil: true</tt> will not cache nil results:
144
+ #
145
+ # cache.fetch('foo') { nil }
146
+ # cache.fetch('bar', skip_nil: true) { nil }
147
+ # cache.exist?('foo') # => true
148
+ # cache.exist?('bar') # => false
149
+ def initialize(error_handler: DEFAULT_ERROR_HANDLER, **redis_options)
150
+ universal_options = redis_options.extract!(*UNIVERSAL_OPTIONS)
151
+
152
+ if pool_options = self.class.send(:retrieve_pool_options, redis_options)
153
+ @redis = ::ConnectionPool.new(pool_options) { self.class.build_redis(**redis_options) }
154
+ else
155
+ @redis = self.class.build_redis(**redis_options)
156
+ end
151
157
 
152
158
  @max_key_bytesize = MAX_KEY_BYTESIZE
153
159
  @error_handler = error_handler
154
160
 
155
- super namespace: namespace,
156
- compress: compress, compress_threshold: compress_threshold,
157
- expires_in: expires_in, race_condition_ttl: race_condition_ttl,
158
- coder: coder
159
- end
160
-
161
- def redis
162
- @redis ||= begin
163
- pool_options = self.class.send(:retrieve_pool_options, redis_options)
164
-
165
- if pool_options.any?
166
- self.class.send(:ensure_connection_pool_added!)
167
- ::ConnectionPool.new(pool_options) { self.class.build_redis(**redis_options) }
168
- else
169
- self.class.build_redis(**redis_options)
170
- end
171
- end
161
+ super(universal_options)
172
162
  end
173
163
 
174
164
  def inspect
175
- instance = @redis || @redis_options
176
- "#<#{self.class} options=#{options.inspect} redis=#{instance.inspect}>"
165
+ "#<#{self.class} options=#{options.inspect} redis=#{redis.inspect}>"
177
166
  end
178
167
 
179
168
  # Cache Store API implementation.
@@ -181,14 +170,13 @@ module ActiveSupport
181
170
  # Read multiple values at once. Returns a hash of requested keys ->
182
171
  # fetched values.
183
172
  def read_multi(*names)
184
- if mget_capable?
185
- instrument(:read_multi, names, options) do |payload|
186
- read_multi_mget(*names).tap do |results|
187
- payload[:hits] = results.keys
188
- end
173
+ return {} if names.empty?
174
+
175
+ options = names.extract_options!
176
+ instrument_multi(:read_multi, names, options) do |payload|
177
+ read_multi_entries(names, **options).tap do |results|
178
+ payload[:hits] = results.keys
189
179
  end
190
- else
191
- super
192
180
  end
193
181
  end
194
182
 
@@ -212,7 +200,7 @@ module ActiveSupport
212
200
  unless String === matcher
213
201
  raise ArgumentError, "Only Redis glob strings are supported: #{matcher.inspect}"
214
202
  end
215
- redis.with do |c|
203
+ redis.then do |c|
216
204
  pattern = namespace_key(matcher, options)
217
205
  cursor = "0"
218
206
  # Fetch keys in batches using SCAN to avoid blocking the Redis server.
@@ -228,12 +216,21 @@ module ActiveSupport
228
216
  end
229
217
  end
230
218
 
231
- # Cache Store API implementation.
219
+ # Increment a cached integer value using the Redis incrby atomic operator.
220
+ # Returns the updated value.
221
+ #
222
+ # If the key is unset or has expired, it will be set to +amount+:
232
223
  #
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.
224
+ # cache.increment("foo") # => 1
225
+ # cache.increment("bar", 100) # => 100
226
+ #
227
+ # To set a specific value, call #write passing <tt>raw: true</tt>:
228
+ #
229
+ # cache.write("baz", 5, raw: true)
230
+ # cache.increment("baz") # => 6
231
+ #
232
+ # Incrementing a non-numeric value, or a value written without
233
+ # <tt>raw: true</tt>, will fail and return +nil+.
237
234
  #
238
235
  # Failsafe: Raises errors.
239
236
  def increment(name, amount = 1, options = nil)
@@ -241,22 +238,25 @@ module ActiveSupport
241
238
  failsafe :increment do
242
239
  options = merged_options(options)
243
240
  key = normalize_key(name, options)
244
-
245
- redis.with do |c|
246
- c.incrby(key, amount).tap do
247
- write_key_expiry(c, key, options)
248
- end
249
- end
241
+ change_counter(key, amount, options)
250
242
  end
251
243
  end
252
244
  end
253
245
 
254
- # Cache Store API implementation.
246
+ # Decrement a cached integer value using the Redis decrby atomic operator.
247
+ # Returns the updated value.
248
+ #
249
+ # If the key is unset or has expired, it will be set to +-amount+:
250
+ #
251
+ # cache.decrement("foo") # => -1
255
252
  #
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.
253
+ # To set a specific value, call #write passing <tt>raw: true</tt>:
254
+ #
255
+ # cache.write("baz", 5, raw: true)
256
+ # cache.decrement("baz") # => 4
257
+ #
258
+ # Decrementing a non-numeric value, or a value written without
259
+ # <tt>raw: true</tt>, will fail and return +nil+.
260
260
  #
261
261
  # Failsafe: Raises errors.
262
262
  def decrement(name, amount = 1, options = nil)
@@ -264,12 +264,7 @@ module ActiveSupport
264
264
  failsafe :decrement do
265
265
  options = merged_options(options)
266
266
  key = normalize_key(name, options)
267
-
268
- redis.with do |c|
269
- c.decrby(key, amount).tap do
270
- write_key_expiry(c, key, options)
271
- end
272
- end
267
+ change_counter(key, -amount, options)
273
268
  end
274
269
  end
275
270
  end
@@ -291,36 +286,27 @@ module ActiveSupport
291
286
  if namespace = merged_options(options)[:namespace]
292
287
  delete_matched "*", namespace: namespace
293
288
  else
294
- redis.with { |c| c.flushdb }
289
+ redis.then { |c| c.flushdb }
295
290
  end
296
291
  end
297
292
  end
298
293
 
299
294
  # Get info from redis servers.
300
295
  def stats
301
- redis.with { |c| c.info }
302
- end
303
-
304
- def mget_capable? # :nodoc:
305
- set_redis_capabilities unless defined? @mget_capable
306
- @mget_capable
307
- end
308
-
309
- def mset_capable? # :nodoc:
310
- set_redis_capabilities unless defined? @mset_capable
311
- @mset_capable
296
+ redis.then { |c| c.info }
312
297
  end
313
298
 
314
299
  private
315
- def set_redis_capabilities
316
- 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
300
+ def pipeline_entries(entries, &block)
301
+ redis.then { |c|
302
+ if c.is_a?(Redis::Distributed)
303
+ entries.group_by { |k, _v| c.node_for(k) }.each do |node, sub_entries|
304
+ node.pipelined { |pipe| yield(pipe, sub_entries) }
305
+ end
306
+ else
307
+ c.pipelined { |pipe| yield(pipe, entries) }
308
+ end
309
+ }
324
310
  end
325
311
 
326
312
  # Store provider interface:
@@ -331,35 +317,29 @@ module ActiveSupport
331
317
 
332
318
  def read_serialized_entry(key, raw: false, **options)
333
319
  failsafe :read_entry do
334
- redis.with { |c| c.get(key) }
320
+ redis.then { |c| c.get(key) }
335
321
  end
336
322
  end
337
323
 
338
324
  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
325
  options = merged_options(options)
349
326
  return {} if names == []
350
327
  raw = options&.fetch(:raw, false)
351
328
 
352
329
  keys = names.map { |name| normalize_key(name, options) }
353
330
 
354
- values = failsafe(:read_multi_mget, returning: {}) do
355
- redis.with { |c| c.mget(*keys) }
331
+ values = failsafe(:read_multi_entries, returning: {}) do
332
+ redis.then { |c| c.mget(*keys) }
356
333
  end
357
334
 
358
335
  names.zip(values).each_with_object({}) do |(name, value), results|
359
336
  if value
360
337
  entry = deserialize_entry(value, raw: raw)
361
338
  unless entry.nil? || entry.expired? || entry.mismatched?(normalize_version(name, options))
362
- results[name] = entry.value
339
+ begin
340
+ results[name] = entry.value
341
+ rescue DeserializationError
342
+ end
363
343
  end
364
344
  end
365
345
  end
@@ -372,7 +352,7 @@ module ActiveSupport
372
352
  write_serialized_entry(key, serialize_entry(entry, raw: raw, **options), raw: raw, **options)
373
353
  end
374
354
 
375
- def write_serialized_entry(key, payload, raw: false, unless_exist: false, expires_in: nil, race_condition_ttl: nil, **options)
355
+ def write_serialized_entry(key, payload, raw: false, unless_exist: false, expires_in: nil, race_condition_ttl: nil, pipeline: nil, **options)
376
356
  # If race condition TTL is in use, ensure that cache entries
377
357
  # stick around a bit longer after they would have expired
378
358
  # so we can purposefully serve stale entries.
@@ -386,41 +366,40 @@ module ActiveSupport
386
366
  modifiers[:px] = (1000 * expires_in.to_f).ceil if expires_in
387
367
  end
388
368
 
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
369
+ if pipeline
370
+ pipeline.set(key, payload, **modifiers)
371
+ else
372
+ failsafe :write_entry, returning: false do
373
+ redis.then { |c| c.set key, payload, **modifiers }
374
+ end
397
375
  end
398
376
  end
399
377
 
400
378
  # Delete an entry from the cache.
401
- def delete_entry(key, options)
379
+ def delete_entry(key, **options)
402
380
  failsafe :delete_entry, returning: false do
403
- redis.with { |c| c.del key }
381
+ redis.then { |c| c.del(key) == 1 }
404
382
  end
405
383
  end
406
384
 
407
385
  # Deletes multiple entries in the cache. Returns the number of entries deleted.
408
386
  def delete_multi_entries(entries, **_options)
409
- redis.with { |c| c.del(entries) }
387
+ failsafe :delete_multi_entries, returning: 0 do
388
+ redis.then { |c| c.del(entries) }
389
+ end
410
390
  end
411
391
 
412
392
  # 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
393
+ def write_multi_entries(entries, **options)
394
+ return if entries.empty?
395
+
396
+ failsafe :write_multi_entries do
397
+ pipeline_entries(entries) do |pipeline, sharded_entries|
398
+ options = options.dup
399
+ options[:pipeline] = pipeline
400
+ sharded_entries.each do |key, entry|
401
+ write_entry key, entry, **options
421
402
  end
422
- else
423
- super
424
403
  end
425
404
  end
426
405
  end
@@ -462,10 +441,43 @@ module ActiveSupport
462
441
  end
463
442
  end
464
443
 
444
+ def change_counter(key, amount, options)
445
+ redis.then do |c|
446
+ c = c.node_for(key) if c.is_a?(Redis::Distributed)
447
+
448
+ expires_in = options[:expires_in]
449
+
450
+ if expires_in
451
+ if supports_expire_nx?
452
+ count, _ = c.pipelined do |pipeline|
453
+ pipeline.incrby(key, amount)
454
+ pipeline.call(:expire, key, expires_in.to_i, "NX")
455
+ end
456
+ else
457
+ count, ttl = c.pipelined do |pipeline|
458
+ pipeline.incrby(key, amount)
459
+ pipeline.ttl(key)
460
+ end
461
+ c.expire(key, expires_in.to_i) if ttl < 0
462
+ end
463
+ else
464
+ count = c.incrby(key, amount)
465
+ end
466
+
467
+ count
468
+ end
469
+ end
470
+
471
+ def supports_expire_nx?
472
+ return @supports_expire_nx if defined?(@supports_expire_nx)
473
+
474
+ redis_versions = redis.then { |c| Array.wrap(c.info("server")).pluck("redis_version") }
475
+ @supports_expire_nx = redis_versions.all? { |v| Gem::Version.new(v) >= Gem::Version.new("7.0.0") }
476
+ end
477
+
465
478
  def failsafe(method, returning: nil)
466
479
  yield
467
480
  rescue ::Redis::BaseError => error
468
- ActiveSupport.error_reporter&.report(error, handled: true, severity: :warning)
469
481
  @error_handler&.call(method: method, exception: error, returning: returning)
470
482
  returning
471
483
  end