activesupport 6.0.6.1 → 7.1.3.2

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 (245) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +865 -438
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +6 -6
  5. data/lib/active_support/actionable_error.rb +4 -2
  6. data/lib/active_support/array_inquirer.rb +4 -2
  7. data/lib/active_support/backtrace_cleaner.rb +30 -10
  8. data/lib/active_support/benchmarkable.rb +4 -3
  9. data/lib/active_support/broadcast_logger.rb +250 -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 +53 -20
  14. data/lib/active_support/cache/mem_cache_store.rb +208 -63
  15. data/lib/active_support/cache/memory_store.rb +120 -38
  16. data/lib/active_support/cache/null_store.rb +16 -2
  17. data/lib/active_support/cache/redis_cache_store.rb +201 -208
  18. data/lib/active_support/cache/serializer_with_fallback.rb +175 -0
  19. data/lib/active_support/cache/strategy/local_cache.rb +73 -66
  20. data/lib/active_support/cache.rb +539 -261
  21. data/lib/active_support/callbacks.rb +273 -142
  22. data/lib/active_support/code_generator.rb +65 -0
  23. data/lib/active_support/concern.rb +53 -7
  24. data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +44 -7
  25. data/lib/active_support/concurrency/null_lock.rb +13 -0
  26. data/lib/active_support/concurrency/share_lock.rb +2 -2
  27. data/lib/active_support/configurable.rb +19 -6
  28. data/lib/active_support/configuration_file.rb +51 -0
  29. data/lib/active_support/core_ext/array/access.rb +1 -5
  30. data/lib/active_support/core_ext/array/conversions.rb +15 -13
  31. data/lib/active_support/core_ext/array/grouping.rb +6 -6
  32. data/lib/active_support/core_ext/array/inquiry.rb +2 -2
  33. data/lib/active_support/core_ext/benchmark.rb +2 -2
  34. data/lib/active_support/core_ext/big_decimal/conversions.rb +1 -1
  35. data/lib/active_support/core_ext/class/attribute.rb +34 -44
  36. data/lib/active_support/core_ext/class/subclasses.rb +19 -29
  37. data/lib/active_support/core_ext/date/blank.rb +1 -1
  38. data/lib/active_support/core_ext/date/calculations.rb +24 -9
  39. data/lib/active_support/core_ext/date/conversions.rb +18 -16
  40. data/lib/active_support/core_ext/date_and_time/calculations.rb +27 -4
  41. data/lib/active_support/core_ext/date_and_time/compatibility.rb +15 -0
  42. data/lib/active_support/core_ext/date_time/blank.rb +1 -1
  43. data/lib/active_support/core_ext/date_time/calculations.rb +4 -0
  44. data/lib/active_support/core_ext/date_time/conversions.rb +19 -15
  45. data/lib/active_support/core_ext/digest/uuid.rb +30 -13
  46. data/lib/active_support/core_ext/enumerable.rb +146 -72
  47. data/lib/active_support/core_ext/erb/util.rb +196 -0
  48. data/lib/active_support/core_ext/file/atomic.rb +3 -1
  49. data/lib/active_support/core_ext/hash/conversions.rb +3 -4
  50. data/lib/active_support/core_ext/hash/deep_merge.rb +22 -14
  51. data/lib/active_support/core_ext/hash/deep_transform_values.rb +4 -4
  52. data/lib/active_support/core_ext/hash/indifferent_access.rb +3 -3
  53. data/lib/active_support/core_ext/hash/keys.rb +5 -5
  54. data/lib/active_support/core_ext/hash/slice.rb +3 -2
  55. data/lib/active_support/core_ext/integer/inflections.rb +12 -12
  56. data/lib/active_support/core_ext/kernel/reporting.rb +4 -4
  57. data/lib/active_support/core_ext/kernel/singleton_class.rb +1 -1
  58. data/lib/active_support/core_ext/load_error.rb +1 -1
  59. data/lib/active_support/core_ext/module/attr_internal.rb +2 -2
  60. data/lib/active_support/core_ext/module/attribute_accessors.rb +31 -29
  61. data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +51 -20
  62. data/lib/active_support/core_ext/module/concerning.rb +14 -8
  63. data/lib/active_support/core_ext/module/delegation.rb +75 -42
  64. data/lib/active_support/core_ext/module/deprecation.rb +15 -12
  65. data/lib/active_support/core_ext/module/introspection.rb +1 -26
  66. data/lib/active_support/core_ext/name_error.rb +23 -2
  67. data/lib/active_support/core_ext/numeric/bytes.rb +9 -0
  68. data/lib/active_support/core_ext/numeric/conversions.rb +82 -73
  69. data/lib/active_support/core_ext/object/acts_like.rb +29 -5
  70. data/lib/active_support/core_ext/object/blank.rb +2 -2
  71. data/lib/active_support/core_ext/object/deep_dup.rb +17 -1
  72. data/lib/active_support/core_ext/object/duplicable.rb +15 -4
  73. data/lib/active_support/core_ext/object/inclusion.rb +13 -5
  74. data/lib/active_support/core_ext/object/instance_variables.rb +22 -12
  75. data/lib/active_support/core_ext/object/json.rb +52 -28
  76. data/lib/active_support/core_ext/object/to_query.rb +2 -4
  77. data/lib/active_support/core_ext/object/try.rb +20 -20
  78. data/lib/active_support/core_ext/object/with.rb +44 -0
  79. data/lib/active_support/core_ext/object/with_options.rb +25 -6
  80. data/lib/active_support/core_ext/object.rb +1 -0
  81. data/lib/active_support/core_ext/pathname/blank.rb +16 -0
  82. data/lib/active_support/core_ext/pathname/existence.rb +23 -0
  83. data/lib/active_support/core_ext/pathname.rb +4 -0
  84. data/lib/active_support/core_ext/range/compare_range.rb +6 -25
  85. data/lib/active_support/core_ext/range/conversions.rb +34 -13
  86. data/lib/active_support/core_ext/range/each.rb +1 -1
  87. data/lib/active_support/core_ext/range/overlap.rb +40 -0
  88. data/lib/active_support/core_ext/range.rb +1 -2
  89. data/lib/active_support/core_ext/regexp.rb +8 -1
  90. data/lib/active_support/core_ext/securerandom.rb +25 -13
  91. data/lib/active_support/core_ext/string/access.rb +5 -24
  92. data/lib/active_support/core_ext/string/conversions.rb +3 -2
  93. data/lib/active_support/core_ext/string/filters.rb +21 -15
  94. data/lib/active_support/core_ext/string/indent.rb +1 -1
  95. data/lib/active_support/core_ext/string/inflections.rb +51 -10
  96. data/lib/active_support/core_ext/string/inquiry.rb +2 -1
  97. data/lib/active_support/core_ext/string/multibyte.rb +2 -2
  98. data/lib/active_support/core_ext/string/output_safety.rb +85 -194
  99. data/lib/active_support/core_ext/string/starts_ends_with.rb +2 -2
  100. data/lib/active_support/core_ext/symbol/starts_ends_with.rb +6 -0
  101. data/lib/active_support/core_ext/symbol.rb +3 -0
  102. data/lib/active_support/core_ext/thread/backtrace/location.rb +12 -0
  103. data/lib/active_support/core_ext/time/calculations.rb +46 -8
  104. data/lib/active_support/core_ext/time/conversions.rb +16 -13
  105. data/lib/active_support/core_ext/time/zones.rb +12 -28
  106. data/lib/active_support/core_ext.rb +2 -1
  107. data/lib/active_support/current_attributes/test_helper.rb +13 -0
  108. data/lib/active_support/current_attributes.rb +54 -22
  109. data/lib/active_support/deep_mergeable.rb +53 -0
  110. data/lib/active_support/dependencies/autoload.rb +17 -12
  111. data/lib/active_support/dependencies/interlock.rb +10 -18
  112. data/lib/active_support/dependencies/require_dependency.rb +28 -0
  113. data/lib/active_support/dependencies.rb +58 -769
  114. data/lib/active_support/deprecation/behaviors.rb +77 -38
  115. data/lib/active_support/deprecation/constant_accessor.rb +5 -4
  116. data/lib/active_support/deprecation/deprecators.rb +104 -0
  117. data/lib/active_support/deprecation/disallowed.rb +54 -0
  118. data/lib/active_support/deprecation/instance_delegator.rb +31 -5
  119. data/lib/active_support/deprecation/method_wrappers.rb +12 -28
  120. data/lib/active_support/deprecation/proxy_wrappers.rb +40 -25
  121. data/lib/active_support/deprecation/reporting.rb +76 -16
  122. data/lib/active_support/deprecation.rb +36 -4
  123. data/lib/active_support/deprecator.rb +7 -0
  124. data/lib/active_support/descendants_tracker.rb +150 -68
  125. data/lib/active_support/digest.rb +5 -3
  126. data/lib/active_support/duration/iso8601_parser.rb +3 -3
  127. data/lib/active_support/duration/iso8601_serializer.rb +24 -12
  128. data/lib/active_support/duration.rb +136 -56
  129. data/lib/active_support/encrypted_configuration.rb +72 -9
  130. data/lib/active_support/encrypted_file.rb +46 -13
  131. data/lib/active_support/environment_inquirer.rb +40 -0
  132. data/lib/active_support/error_reporter/test_helper.rb +15 -0
  133. data/lib/active_support/error_reporter.rb +203 -0
  134. data/lib/active_support/evented_file_update_checker.rb +86 -137
  135. data/lib/active_support/execution_context/test_helper.rb +13 -0
  136. data/lib/active_support/execution_context.rb +53 -0
  137. data/lib/active_support/execution_wrapper.rb +31 -12
  138. data/lib/active_support/executor/test_helper.rb +7 -0
  139. data/lib/active_support/file_update_checker.rb +4 -2
  140. data/lib/active_support/fork_tracker.rb +79 -0
  141. data/lib/active_support/gem_version.rb +5 -5
  142. data/lib/active_support/gzip.rb +2 -0
  143. data/lib/active_support/hash_with_indifferent_access.rb +86 -42
  144. data/lib/active_support/html_safe_translation.rb +53 -0
  145. data/lib/active_support/i18n.rb +2 -1
  146. data/lib/active_support/i18n_railtie.rb +29 -27
  147. data/lib/active_support/inflector/inflections.rb +26 -9
  148. data/lib/active_support/inflector/methods.rb +54 -64
  149. data/lib/active_support/inflector/transliterate.rb +7 -5
  150. data/lib/active_support/isolated_execution_state.rb +76 -0
  151. data/lib/active_support/json/decoding.rb +6 -5
  152. data/lib/active_support/json/encoding.rb +31 -45
  153. data/lib/active_support/key_generator.rb +32 -7
  154. data/lib/active_support/lazy_load_hooks.rb +33 -7
  155. data/lib/active_support/locale/en.yml +10 -4
  156. data/lib/active_support/log_subscriber/test_helper.rb +2 -2
  157. data/lib/active_support/log_subscriber.rb +101 -32
  158. data/lib/active_support/logger.rb +9 -60
  159. data/lib/active_support/logger_silence.rb +2 -26
  160. data/lib/active_support/logger_thread_safe_level.rb +24 -25
  161. data/lib/active_support/message_encryptor.rb +205 -58
  162. data/lib/active_support/message_encryptors.rb +141 -0
  163. data/lib/active_support/message_pack/cache_serializer.rb +23 -0
  164. data/lib/active_support/message_pack/extensions.rb +292 -0
  165. data/lib/active_support/message_pack/serializer.rb +63 -0
  166. data/lib/active_support/message_pack.rb +50 -0
  167. data/lib/active_support/message_verifier.rb +237 -86
  168. data/lib/active_support/message_verifiers.rb +135 -0
  169. data/lib/active_support/messages/codec.rb +65 -0
  170. data/lib/active_support/messages/metadata.rb +112 -46
  171. data/lib/active_support/messages/rotation_configuration.rb +2 -1
  172. data/lib/active_support/messages/rotation_coordinator.rb +93 -0
  173. data/lib/active_support/messages/rotator.rb +35 -32
  174. data/lib/active_support/messages/serializer_with_fallback.rb +158 -0
  175. data/lib/active_support/multibyte/chars.rb +15 -52
  176. data/lib/active_support/multibyte/unicode.rb +8 -122
  177. data/lib/active_support/multibyte.rb +1 -1
  178. data/lib/active_support/notifications/fanout.rb +310 -105
  179. data/lib/active_support/notifications/instrumenter.rb +113 -48
  180. data/lib/active_support/notifications.rb +56 -29
  181. data/lib/active_support/number_helper/number_converter.rb +15 -8
  182. data/lib/active_support/number_helper/number_to_currency_converter.rb +11 -6
  183. data/lib/active_support/number_helper/number_to_delimited_converter.rb +1 -1
  184. data/lib/active_support/number_helper/number_to_human_converter.rb +1 -1
  185. data/lib/active_support/number_helper/number_to_human_size_converter.rb +5 -5
  186. data/lib/active_support/number_helper/number_to_phone_converter.rb +2 -1
  187. data/lib/active_support/number_helper/number_to_rounded_converter.rb +9 -5
  188. data/lib/active_support/number_helper/rounding_helper.rb +12 -32
  189. data/lib/active_support/number_helper.rb +379 -304
  190. data/lib/active_support/option_merger.rb +11 -18
  191. data/lib/active_support/ordered_hash.rb +4 -4
  192. data/lib/active_support/ordered_options.rb +23 -3
  193. data/lib/active_support/parameter_filter.rb +104 -75
  194. data/lib/active_support/proxy_object.rb +2 -0
  195. data/lib/active_support/rails.rb +1 -4
  196. data/lib/active_support/railtie.rb +90 -6
  197. data/lib/active_support/reloader.rb +12 -4
  198. data/lib/active_support/rescuable.rb +18 -16
  199. data/lib/active_support/ruby_features.rb +7 -0
  200. data/lib/active_support/secure_compare_rotator.rb +58 -0
  201. data/lib/active_support/security_utils.rb +19 -12
  202. data/lib/active_support/string_inquirer.rb +5 -3
  203. data/lib/active_support/subscriber.rb +23 -47
  204. data/lib/active_support/syntax_error_proxy.rb +70 -0
  205. data/lib/active_support/tagged_logging.rb +84 -23
  206. data/lib/active_support/test_case.rb +166 -27
  207. data/lib/active_support/testing/assertions.rb +73 -20
  208. data/lib/active_support/testing/autorun.rb +0 -2
  209. data/lib/active_support/testing/constant_stubbing.rb +32 -0
  210. data/lib/active_support/testing/deprecation.rb +53 -2
  211. data/lib/active_support/testing/error_reporter_assertions.rb +107 -0
  212. data/lib/active_support/testing/isolation.rb +30 -29
  213. data/lib/active_support/testing/method_call_assertions.rb +24 -11
  214. data/lib/active_support/testing/parallelization/server.rb +82 -0
  215. data/lib/active_support/testing/parallelization/worker.rb +103 -0
  216. data/lib/active_support/testing/parallelization.rb +16 -95
  217. data/lib/active_support/testing/parallelize_executor.rb +81 -0
  218. data/lib/active_support/testing/stream.rb +4 -6
  219. data/lib/active_support/testing/strict_warnings.rb +39 -0
  220. data/lib/active_support/testing/tagged_logging.rb +1 -1
  221. data/lib/active_support/testing/time_helpers.rb +89 -19
  222. data/lib/active_support/time_with_zone.rb +105 -70
  223. data/lib/active_support/values/time_zone.rb +59 -26
  224. data/lib/active_support/version.rb +1 -1
  225. data/lib/active_support/xml_mini/jdom.rb +4 -11
  226. data/lib/active_support/xml_mini/libxml.rb +5 -5
  227. data/lib/active_support/xml_mini/libxmlsax.rb +1 -1
  228. data/lib/active_support/xml_mini/nokogiri.rb +5 -5
  229. data/lib/active_support/xml_mini/nokogirisax.rb +2 -2
  230. data/lib/active_support/xml_mini/rexml.rb +9 -2
  231. data/lib/active_support/xml_mini.rb +7 -6
  232. data/lib/active_support.rb +40 -1
  233. metadata +127 -40
  234. data/lib/active_support/core_ext/array/prepend_and_append.rb +0 -5
  235. data/lib/active_support/core_ext/hash/compact.rb +0 -5
  236. data/lib/active_support/core_ext/hash/transform_values.rb +0 -5
  237. data/lib/active_support/core_ext/marshal.rb +0 -24
  238. data/lib/active_support/core_ext/module/reachable.rb +0 -6
  239. data/lib/active_support/core_ext/numeric/inquiry.rb +0 -5
  240. data/lib/active_support/core_ext/range/include_range.rb +0 -9
  241. data/lib/active_support/core_ext/range/include_time_with_zone.rb +0 -23
  242. data/lib/active_support/core_ext/range/overlaps.rb +0 -10
  243. data/lib/active_support/core_ext/uri.rb +0 -25
  244. data/lib/active_support/dependencies/zeitwerk_integration.rb +0 -117
  245. data/lib/active_support/per_thread_registry.rb +0 -60
@@ -5,61 +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
-
18
- require "digest/sha2"
19
- require "active_support/core_ext/marshal"
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"
16
+ require "active_support/digest"
20
17
 
21
18
  module ActiveSupport
22
19
  module Cache
23
- module ConnectionPoolLike
24
- def with
25
- yield self
26
- end
27
- end
28
-
29
- ::Redis.include(ConnectionPoolLike)
30
- ::Redis::Distributed.include(ConnectionPoolLike)
31
-
32
- # Redis cache store.
20
+ # = Redis \Cache \Store
33
21
  #
34
- # Deployment note: Take care to use a *dedicated Redis cache* rather
35
- # than pointing this at your existing Redis server. It won't cope well
36
- # 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.
37
26
  #
38
27
  # Redis cache server setup guide: https://redis.io/topics/lru-cache
39
28
  #
40
- # * Supports vanilla Redis, hiredis, and Redis::Distributed.
41
- # * 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+.
42
31
  # * Fault tolerant. If the Redis server is unavailable, no exceptions are
43
32
  # raised. Cache fetches are all misses and writes are dropped.
44
33
  # * Local cache. Hot in-memory primary cache within block/middleware scope.
45
- # * +read_multi+ and +write_multi+ support for Redis mget/mset. Use Redis::Distributed
46
- # 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.
47
36
  # * +delete_matched+ support for Redis KEYS globs.
48
37
  class RedisCacheStore < Store
49
- # Keys are truncated with their own SHA2 digest if they exceed 1kB
38
+ # Keys are truncated with the Active Support digest if they exceed 1kB
50
39
  MAX_KEY_BYTESIZE = 1024
51
40
 
52
41
  DEFAULT_REDIS_OPTIONS = {
53
- connect_timeout: 20,
42
+ connect_timeout: 1,
54
43
  read_timeout: 1,
55
44
  write_timeout: 1,
56
- reconnect_attempts: 0,
57
45
  }
58
46
 
59
47
  DEFAULT_ERROR_HANDLER = -> (method:, returning:, exception:) do
60
48
  if logger
61
49
  logger.error { "RedisCacheStore: #{method} failed, returned #{returning.inspect}: #{exception.class}: #{exception.message}" }
62
50
  end
51
+ ActiveSupport.error_reporter&.report(
52
+ exception,
53
+ severity: :warning,
54
+ source: "redis_cache_store.active_support",
55
+ )
63
56
  end
64
57
 
65
58
  # The maximum number of entries to receive per SCAN call.
@@ -71,35 +64,7 @@ module ActiveSupport
71
64
  true
72
65
  end
73
66
 
74
- # Support raw values in the local cache strategy.
75
- module LocalCacheWithRaw # :nodoc:
76
- private
77
- def write_entry(key, entry, **options)
78
- if options[:raw] && local_cache
79
- raw_entry = Entry.new(serialize_entry(entry, raw: true))
80
- raw_entry.expires_at = entry.expires_at
81
- super(key, raw_entry, **options)
82
- else
83
- super
84
- end
85
- end
86
-
87
- def write_multi_entries(entries, **options)
88
- if options[:raw] && local_cache
89
- raw_entries = entries.map do |key, entry|
90
- raw_entry = Entry.new(serialize_entry(entry, raw: true))
91
- raw_entry.expires_at = entry.expires_at
92
- end.to_h
93
-
94
- super(raw_entries, **options)
95
- else
96
- super
97
- end
98
- end
99
- end
100
-
101
67
  prepend Strategy::LocalCache
102
- prepend LocalCacheWithRaw
103
68
 
104
69
  class << self
105
70
  # Factory method to create a new Redis instance.
@@ -113,7 +78,7 @@ module ActiveSupport
113
78
  # :url String -> Redis.new(url: …)
114
79
  # :url Array -> Redis::Distributed.new([{ url: … }, { url: … }, …])
115
80
  #
116
- def build_redis(redis: nil, url: nil, **redis_options) #:nodoc:
81
+ def build_redis(redis: nil, url: nil, **redis_options) # :nodoc:
117
82
  urls = Array(url)
118
83
 
119
84
  if redis.is_a?(Proc)
@@ -121,9 +86,11 @@ module ActiveSupport
121
86
  elsif redis
122
87
  redis
123
88
  elsif urls.size > 1
124
- 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)
125
92
  else
126
- build_redis_client url: urls.first, **redis_options
93
+ build_redis_client(url: urls.first, **redis_options)
127
94
  end
128
95
  end
129
96
 
@@ -134,18 +101,21 @@ module ActiveSupport
134
101
  end
135
102
  end
136
103
 
137
- def build_redis_client(url:, **redis_options)
138
- ::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))
139
106
  end
140
107
  end
141
108
 
142
- attr_reader :redis_options
143
109
  attr_reader :max_key_bytesize
110
+ attr_reader :redis
144
111
 
145
112
  # Creates a new Redis cache store.
146
113
  #
147
- # Handles four options: :redis block, :redis instance, single :url
148
- # 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.
149
119
  #
150
120
  # Option Class Result
151
121
  # :redis Proc -> options[:redis].call
@@ -168,34 +138,31 @@ module ActiveSupport
168
138
  #
169
139
  # Race condition TTL is not set by default. This can be used to avoid
170
140
  # "thundering herd" cache writes when hot cache entries are expired.
171
- # See <tt>ActiveSupport::Cache::Store#fetch</tt> for more.
172
- def initialize(namespace: nil, compress: true, compress_threshold: 1.kilobyte, expires_in: nil, race_condition_ttl: nil, error_handler: DEFAULT_ERROR_HANDLER, **redis_options)
173
- @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
174
157
 
175
158
  @max_key_bytesize = MAX_KEY_BYTESIZE
176
159
  @error_handler = error_handler
177
160
 
178
- super namespace: namespace,
179
- compress: compress, compress_threshold: compress_threshold,
180
- expires_in: expires_in, race_condition_ttl: race_condition_ttl
181
- end
182
-
183
- def redis
184
- @redis ||= begin
185
- pool_options = self.class.send(:retrieve_pool_options, redis_options)
186
-
187
- if pool_options.any?
188
- self.class.send(:ensure_connection_pool_added!)
189
- ::ConnectionPool.new(pool_options) { self.class.build_redis(**redis_options) }
190
- else
191
- self.class.build_redis(**redis_options)
192
- end
193
- end
161
+ super(universal_options)
194
162
  end
195
163
 
196
164
  def inspect
197
- instance = @redis || @redis_options
198
- "<##{self.class} options=#{options.inspect} redis=#{instance.inspect}>"
165
+ "#<#{self.class} options=#{options.inspect} redis=#{redis.inspect}>"
199
166
  end
200
167
 
201
168
  # Cache Store API implementation.
@@ -203,14 +170,13 @@ module ActiveSupport
203
170
  # Read multiple values at once. Returns a hash of requested keys ->
204
171
  # fetched values.
205
172
  def read_multi(*names)
206
- if mget_capable?
207
- instrument(:read_multi, names, options) do |payload|
208
- read_multi_mget(*names).tap do |results|
209
- payload[:hits] = results.keys
210
- 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
211
179
  end
212
- else
213
- super
214
180
  end
215
181
  end
216
182
 
@@ -234,24 +200,37 @@ module ActiveSupport
234
200
  unless String === matcher
235
201
  raise ArgumentError, "Only Redis glob strings are supported: #{matcher.inspect}"
236
202
  end
237
- redis.with do |c|
203
+ redis.then do |c|
238
204
  pattern = namespace_key(matcher, options)
239
205
  cursor = "0"
240
206
  # Fetch keys in batches using SCAN to avoid blocking the Redis server.
241
- begin
242
- cursor, keys = c.scan(cursor, match: pattern, count: SCAN_BATCH_SIZE)
243
- c.del(*keys) unless keys.empty?
244
- end until cursor == "0"
207
+ nodes = c.respond_to?(:nodes) ? c.nodes : [c]
208
+
209
+ nodes.each do |node|
210
+ begin
211
+ cursor, keys = node.scan(cursor, match: pattern, count: SCAN_BATCH_SIZE)
212
+ node.del(*keys) unless keys.empty?
213
+ end until cursor == "0"
214
+ end
245
215
  end
246
216
  end
247
217
  end
248
218
 
249
- # Cache Store API implementation.
219
+ # Increment a cached integer value using the Redis incrby atomic operator.
220
+ # Returns the updated value.
221
+ #
222
+ # If the key is unset or has expired, it will be set to +amount+:
223
+ #
224
+ # cache.increment("foo") # => 1
225
+ # cache.increment("bar", 100) # => 100
226
+ #
227
+ # To set a specific value, call #write passing <tt>raw: true</tt>:
228
+ #
229
+ # cache.write("baz", 5, raw: true)
230
+ # cache.increment("baz") # => 6
250
231
  #
251
- # Increment a cached value. This method uses the Redis incr atomic
252
- # operator and can only be used on values written with the :raw option.
253
- # Calling it on a value not stored with :raw will initialize that value
254
- # to zero.
232
+ # Incrementing a non-numeric value, or a value written without
233
+ # <tt>raw: true</tt>, will fail and return +nil+.
255
234
  #
256
235
  # Failsafe: Raises errors.
257
236
  def increment(name, amount = 1, options = nil)
@@ -259,22 +238,25 @@ module ActiveSupport
259
238
  failsafe :increment do
260
239
  options = merged_options(options)
261
240
  key = normalize_key(name, options)
262
-
263
- redis.with do |c|
264
- c.incrby(key, amount).tap do
265
- write_key_expiry(c, key, options)
266
- end
267
- end
241
+ change_counter(key, amount, options)
268
242
  end
269
243
  end
270
244
  end
271
245
 
272
- # 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+:
273
250
  #
274
- # Decrement a cached value. This method uses the Redis decr atomic
275
- # operator and can only be used on values written with the :raw option.
276
- # Calling it on a value not stored with :raw will initialize that value
277
- # to zero.
251
+ # cache.decrement("foo") # => -1
252
+ #
253
+ # To set a specific value, call #write passing <tt>raw: true</tt>:
254
+ #
255
+ # cache.write("baz", 5, raw: true)
256
+ # cache.decrement("baz") # => 4
257
+ #
258
+ # Decrementing a non-numeric value, or a value written without
259
+ # <tt>raw: true</tt>, will fail and return +nil+.
278
260
  #
279
261
  # Failsafe: Raises errors.
280
262
  def decrement(name, amount = 1, options = nil)
@@ -282,12 +264,7 @@ module ActiveSupport
282
264
  failsafe :decrement do
283
265
  options = merged_options(options)
284
266
  key = normalize_key(name, options)
285
-
286
- redis.with do |c|
287
- c.decrby(key, amount).tap do
288
- write_key_expiry(c, key, options)
289
- end
290
- end
267
+ change_counter(key, -amount, options)
291
268
  end
292
269
  end
293
270
  end
@@ -309,67 +286,60 @@ module ActiveSupport
309
286
  if namespace = merged_options(options)[:namespace]
310
287
  delete_matched "*", namespace: namespace
311
288
  else
312
- redis.with { |c| c.flushdb }
289
+ redis.then { |c| c.flushdb }
313
290
  end
314
291
  end
315
292
  end
316
293
 
317
- def mget_capable? #:nodoc:
318
- set_redis_capabilities unless defined? @mget_capable
319
- @mget_capable
320
- end
321
-
322
- def mset_capable? #:nodoc:
323
- set_redis_capabilities unless defined? @mset_capable
324
- @mset_capable
294
+ # Get info from redis servers.
295
+ def stats
296
+ redis.then { |c| c.info }
325
297
  end
326
298
 
327
299
  private
328
- def set_redis_capabilities
329
- case redis
330
- when Redis::Distributed
331
- @mget_capable = true
332
- @mset_capable = false
333
- else
334
- @mget_capable = true
335
- @mset_capable = true
336
- 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
+ }
337
310
  end
338
311
 
339
312
  # Store provider interface:
340
313
  # Read an entry from the cache.
341
314
  def read_entry(key, **options)
342
- failsafe :read_entry do
343
- raw = options&.fetch(:raw, false)
344
- deserialize_entry(redis.with { |c| c.get(key) }, raw: raw)
345
- end
315
+ deserialize_entry(read_serialized_entry(key, **options), **options)
346
316
  end
347
317
 
348
- def read_multi_entries(names, **options)
349
- if mget_capable?
350
- read_multi_mget(*names, **options)
351
- else
352
- super
318
+ def read_serialized_entry(key, raw: false, **options)
319
+ failsafe :read_entry do
320
+ redis.then { |c| c.get(key) }
353
321
  end
354
322
  end
355
323
 
356
- def read_multi_mget(*names)
357
- options = names.extract_options!
324
+ def read_multi_entries(names, **options)
358
325
  options = merged_options(options)
359
326
  return {} if names == []
360
327
  raw = options&.fetch(:raw, false)
361
328
 
362
329
  keys = names.map { |name| normalize_key(name, options) }
363
330
 
364
- values = failsafe(:read_multi_mget, returning: {}) do
365
- redis.with { |c| c.mget(*keys) }
331
+ values = failsafe(:read_multi_entries, returning: {}) do
332
+ redis.then { |c| c.mget(*keys) }
366
333
  end
367
334
 
368
335
  names.zip(values).each_with_object({}) do |(name, value), results|
369
336
  if value
370
337
  entry = deserialize_entry(value, raw: raw)
371
338
  unless entry.nil? || entry.expired? || entry.mismatched?(normalize_version(name, options))
372
- results[name] = entry.value
339
+ begin
340
+ results[name] = entry.value
341
+ rescue DeserializationError
342
+ end
373
343
  end
374
344
  end
375
345
  end
@@ -378,9 +348,11 @@ module ActiveSupport
378
348
  # Write an entry to the cache.
379
349
  #
380
350
  # Requires Redis 2.6.12+ for extended SET options.
381
- def write_entry(key, entry, unless_exist: false, raw: false, expires_in: nil, race_condition_ttl: nil, **options)
382
- serialized_entry = serialize_entry(entry, raw: raw)
351
+ def write_entry(key, entry, raw: false, **options)
352
+ write_serialized_entry(key, serialize_entry(entry, raw: raw, **options), raw: raw, **options)
353
+ end
383
354
 
355
+ def write_serialized_entry(key, payload, raw: false, unless_exist: false, expires_in: nil, race_condition_ttl: nil, pipeline: nil, **options)
384
356
  # If race condition TTL is in use, ensure that cache entries
385
357
  # stick around a bit longer after they would have expired
386
358
  # so we can purposefully serve stale entries.
@@ -388,53 +360,58 @@ module ActiveSupport
388
360
  expires_in += 5.minutes
389
361
  end
390
362
 
391
- failsafe :write_entry, returning: false do
392
- if unless_exist || expires_in
393
- modifiers = {}
394
- modifiers[:nx] = unless_exist
395
- modifiers[:px] = (1000 * expires_in.to_f).ceil if expires_in
363
+ modifiers = {}
364
+ if unless_exist || expires_in
365
+ modifiers[:nx] = unless_exist
366
+ modifiers[:px] = (1000 * expires_in.to_f).ceil if expires_in
367
+ end
396
368
 
397
- redis.with { |c| c.set key, serialized_entry, **modifiers }
398
- else
399
- redis.with { |c| c.set key, serialized_entry }
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 }
400
374
  end
401
375
  end
402
376
  end
403
377
 
404
- def write_key_expiry(client, key, options)
405
- if options[:expires_in] && client.ttl(key).negative?
406
- client.expire key, options[:expires_in].to_i
378
+ # Delete an entry from the cache.
379
+ def delete_entry(key, **options)
380
+ failsafe :delete_entry, returning: false do
381
+ redis.then { |c| c.del(key) == 1 }
407
382
  end
408
383
  end
409
384
 
410
- # Delete an entry from the cache.
411
- def delete_entry(key, options)
412
- failsafe :delete_entry, returning: false do
413
- redis.with { |c| c.del key }
385
+ # Deletes multiple entries in the cache. Returns the number of entries deleted.
386
+ def delete_multi_entries(entries, **_options)
387
+ failsafe :delete_multi_entries, returning: 0 do
388
+ redis.then { |c| c.del(entries) }
414
389
  end
415
390
  end
416
391
 
417
392
  # Nonstandard store provider API to write multiple values at once.
418
- def write_multi_entries(entries, expires_in: nil, **options)
419
- if entries.any?
420
- if mset_capable? && expires_in.nil?
421
- failsafe :write_multi_entries do
422
- redis.with { |c| c.mapped_mset(serialize_entries(entries, raw: options[:raw])) }
393
+ def write_multi_entries(entries, **options)
394
+ return if entries.empty?
395
+
396
+ failsafe :write_multi_entries do
397
+ pipeline_entries(entries) do |pipeline, sharded_entries|
398
+ options = options.dup
399
+ options[:pipeline] = pipeline
400
+ sharded_entries.each do |key, entry|
401
+ write_entry key, entry, **options
423
402
  end
424
- else
425
- super
426
403
  end
427
404
  end
428
405
  end
429
406
 
430
407
  # Truncate keys that exceed 1kB.
431
408
  def normalize_key(key, options)
432
- truncate_key super.b
409
+ truncate_key super&.b
433
410
  end
434
411
 
435
412
  def truncate_key(key)
436
- if key.bytesize > max_key_bytesize
437
- suffix = ":sha2:#{::Digest::SHA2.hexdigest(key)}"
413
+ if key && key.bytesize > max_key_bytesize
414
+ suffix = ":hash:#{ActiveSupport::Digest.hexdigest(key)}"
438
415
  truncate_at = max_key_bytesize - suffix.bytesize
439
416
  "#{key.byteslice(0, truncate_at)}#{suffix}"
440
417
  else
@@ -442,52 +419,68 @@ module ActiveSupport
442
419
  end
443
420
  end
444
421
 
445
- def deserialize_entry(serialized_entry, raw:)
446
- if serialized_entry
447
- entry = Marshal.load(serialized_entry) rescue serialized_entry
448
-
449
- written_raw = serialized_entry.equal?(entry)
450
- if raw != written_raw
451
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
452
- Using a different value for the raw option when reading and writing
453
- to a cache key is deprecated for :redis_cache_store and Rails 6.0
454
- will stop automatically detecting the format when reading to avoid
455
- marshal loading untrusted raw strings.
456
- MSG
457
- end
458
-
459
- entry.is_a?(Entry) ? entry : Entry.new(entry)
422
+ def deserialize_entry(payload, raw: false, **)
423
+ if raw && !payload.nil?
424
+ Entry.new(payload)
425
+ else
426
+ super(payload)
460
427
  end
461
428
  end
462
429
 
463
- def serialize_entry(entry, raw: false)
430
+ def serialize_entry(entry, raw: false, **options)
464
431
  if raw
465
432
  entry.value.to_s
466
433
  else
467
- Marshal.dump(entry)
434
+ super(entry, raw: raw, **options)
468
435
  end
469
436
  end
470
437
 
471
- def serialize_entries(entries, raw: false)
438
+ def serialize_entries(entries, **options)
472
439
  entries.transform_values do |entry|
473
- serialize_entry entry, raw: raw
440
+ serialize_entry(entry, **options)
441
+ end
442
+ end
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
474
468
  end
475
469
  end
476
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
+
477
478
  def failsafe(method, returning: nil)
478
479
  yield
479
- rescue ::Redis::BaseError => e
480
- handle_exception exception: e, method: method, returning: returning
480
+ rescue ::Redis::BaseError => error
481
+ @error_handler&.call(method: method, exception: error, returning: returning)
481
482
  returning
482
483
  end
483
-
484
- def handle_exception(exception:, method:, returning:)
485
- if @error_handler
486
- @error_handler.(method: method, exception: exception, returning: returning)
487
- end
488
- rescue => failsafe
489
- warn "RedisCacheStore ignored exception in handle_exception: #{failsafe.class}: #{failsafe.message}\n #{failsafe.backtrace.join("\n ")}"
490
- end
491
484
  end
492
485
  end
493
486
  end