activesupport 6.1.0 → 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 (225) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1075 -325
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +7 -7
  5. data/lib/active_support/actionable_error.rb +4 -2
  6. data/lib/active_support/array_inquirer.rb +2 -2
  7. data/lib/active_support/backtrace_cleaner.rb +32 -7
  8. data/lib/active_support/benchmarkable.rb +3 -2
  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 +53 -20
  14. data/lib/active_support/cache/mem_cache_store.rb +201 -62
  15. data/lib/active_support/cache/memory_store.rb +86 -24
  16. data/lib/active_support/cache/null_store.rb +16 -2
  17. data/lib/active_support/cache/redis_cache_store.rb +186 -193
  18. data/lib/active_support/cache/serializer_with_fallback.rb +175 -0
  19. data/lib/active_support/cache/strategy/local_cache.rb +63 -71
  20. data/lib/active_support/cache.rb +487 -249
  21. data/lib/active_support/callbacks.rb +227 -105
  22. data/lib/active_support/code_generator.rb +70 -0
  23. data/lib/active_support/concern.rb +9 -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 +18 -5
  28. data/lib/active_support/configuration_file.rb +7 -2
  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/big_decimal/conversions.rb +1 -1
  34. data/lib/active_support/core_ext/class/subclasses.rb +37 -26
  35. data/lib/active_support/core_ext/date/blank.rb +1 -1
  36. data/lib/active_support/core_ext/date/calculations.rb +24 -9
  37. data/lib/active_support/core_ext/date/conversions.rb +16 -15
  38. data/lib/active_support/core_ext/date_and_time/calculations.rb +14 -4
  39. data/lib/active_support/core_ext/date_and_time/compatibility.rb +1 -1
  40. data/lib/active_support/core_ext/date_time/blank.rb +1 -1
  41. data/lib/active_support/core_ext/date_time/calculations.rb +4 -0
  42. data/lib/active_support/core_ext/date_time/conversions.rb +19 -15
  43. data/lib/active_support/core_ext/digest/uuid.rb +30 -13
  44. data/lib/active_support/core_ext/enumerable.rb +85 -83
  45. data/lib/active_support/core_ext/erb/util.rb +196 -0
  46. data/lib/active_support/core_ext/file/atomic.rb +3 -1
  47. data/lib/active_support/core_ext/hash/conversions.rb +1 -2
  48. data/lib/active_support/core_ext/hash/deep_merge.rb +22 -14
  49. data/lib/active_support/core_ext/hash/deep_transform_values.rb +3 -3
  50. data/lib/active_support/core_ext/hash/indifferent_access.rb +3 -3
  51. data/lib/active_support/core_ext/hash/keys.rb +4 -4
  52. data/lib/active_support/core_ext/integer/inflections.rb +12 -12
  53. data/lib/active_support/core_ext/kernel/reporting.rb +4 -4
  54. data/lib/active_support/core_ext/kernel/singleton_class.rb +1 -1
  55. data/lib/active_support/core_ext/module/attribute_accessors.rb +8 -0
  56. data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +49 -22
  57. data/lib/active_support/core_ext/module/concerning.rb +6 -6
  58. data/lib/active_support/core_ext/module/delegation.rb +81 -43
  59. data/lib/active_support/core_ext/module/deprecation.rb +15 -12
  60. data/lib/active_support/core_ext/module/introspection.rb +0 -1
  61. data/lib/active_support/core_ext/name_error.rb +2 -8
  62. data/lib/active_support/core_ext/numeric/bytes.rb +9 -0
  63. data/lib/active_support/core_ext/numeric/conversions.rb +82 -77
  64. data/lib/active_support/core_ext/object/acts_like.rb +29 -5
  65. data/lib/active_support/core_ext/object/blank.rb +2 -2
  66. data/lib/active_support/core_ext/object/deep_dup.rb +17 -1
  67. data/lib/active_support/core_ext/object/duplicable.rb +31 -11
  68. data/lib/active_support/core_ext/object/inclusion.rb +13 -5
  69. data/lib/active_support/core_ext/object/instance_variables.rb +22 -12
  70. data/lib/active_support/core_ext/object/json.rb +49 -27
  71. data/lib/active_support/core_ext/object/to_query.rb +2 -4
  72. data/lib/active_support/core_ext/object/try.rb +20 -20
  73. data/lib/active_support/core_ext/object/with.rb +44 -0
  74. data/lib/active_support/core_ext/object/with_options.rb +25 -6
  75. data/lib/active_support/core_ext/object.rb +1 -0
  76. data/lib/active_support/core_ext/pathname/blank.rb +16 -0
  77. data/lib/active_support/core_ext/pathname/existence.rb +23 -0
  78. data/lib/active_support/core_ext/pathname.rb +4 -0
  79. data/lib/active_support/core_ext/range/compare_range.rb +0 -25
  80. data/lib/active_support/core_ext/range/conversions.rb +34 -13
  81. data/lib/active_support/core_ext/range/each.rb +1 -1
  82. data/lib/active_support/core_ext/range/overlap.rb +40 -0
  83. data/lib/active_support/core_ext/range.rb +1 -2
  84. data/lib/active_support/core_ext/securerandom.rb +25 -13
  85. data/lib/active_support/core_ext/string/conversions.rb +2 -2
  86. data/lib/active_support/core_ext/string/filters.rb +21 -15
  87. data/lib/active_support/core_ext/string/indent.rb +1 -1
  88. data/lib/active_support/core_ext/string/inflections.rb +17 -10
  89. data/lib/active_support/core_ext/string/inquiry.rb +1 -1
  90. data/lib/active_support/core_ext/string/output_safety.rb +85 -165
  91. data/lib/active_support/core_ext/symbol/starts_ends_with.rb +0 -8
  92. data/lib/active_support/core_ext/thread/backtrace/location.rb +12 -0
  93. data/lib/active_support/core_ext/time/calculations.rb +30 -8
  94. data/lib/active_support/core_ext/time/conversions.rb +15 -13
  95. data/lib/active_support/core_ext/time/zones.rb +12 -28
  96. data/lib/active_support/core_ext.rb +2 -1
  97. data/lib/active_support/current_attributes.rb +47 -20
  98. data/lib/active_support/deep_mergeable.rb +53 -0
  99. data/lib/active_support/dependencies/autoload.rb +17 -12
  100. data/lib/active_support/dependencies/interlock.rb +10 -18
  101. data/lib/active_support/dependencies/require_dependency.rb +28 -0
  102. data/lib/active_support/dependencies.rb +58 -788
  103. data/lib/active_support/deprecation/behaviors.rb +66 -40
  104. data/lib/active_support/deprecation/constant_accessor.rb +5 -4
  105. data/lib/active_support/deprecation/deprecators.rb +104 -0
  106. data/lib/active_support/deprecation/disallowed.rb +6 -8
  107. data/lib/active_support/deprecation/instance_delegator.rb +31 -4
  108. data/lib/active_support/deprecation/method_wrappers.rb +9 -26
  109. data/lib/active_support/deprecation/proxy_wrappers.rb +38 -23
  110. data/lib/active_support/deprecation/reporting.rb +43 -26
  111. data/lib/active_support/deprecation.rb +32 -5
  112. data/lib/active_support/deprecator.rb +7 -0
  113. data/lib/active_support/descendants_tracker.rb +150 -72
  114. data/lib/active_support/digest.rb +5 -3
  115. data/lib/active_support/duration/iso8601_parser.rb +3 -3
  116. data/lib/active_support/duration/iso8601_serializer.rb +9 -3
  117. data/lib/active_support/duration.rb +83 -52
  118. data/lib/active_support/encrypted_configuration.rb +72 -9
  119. data/lib/active_support/encrypted_file.rb +29 -13
  120. data/lib/active_support/environment_inquirer.rb +23 -3
  121. data/lib/active_support/error_reporter/test_helper.rb +15 -0
  122. data/lib/active_support/error_reporter.rb +203 -0
  123. data/lib/active_support/evented_file_update_checker.rb +20 -7
  124. data/lib/active_support/execution_context/test_helper.rb +13 -0
  125. data/lib/active_support/execution_context.rb +53 -0
  126. data/lib/active_support/execution_wrapper.rb +44 -22
  127. data/lib/active_support/executor/test_helper.rb +7 -0
  128. data/lib/active_support/file_update_checker.rb +4 -2
  129. data/lib/active_support/fork_tracker.rb +28 -11
  130. data/lib/active_support/gem_version.rb +4 -4
  131. data/lib/active_support/gzip.rb +2 -0
  132. data/lib/active_support/hash_with_indifferent_access.rb +44 -19
  133. data/lib/active_support/html_safe_translation.rb +53 -0
  134. data/lib/active_support/i18n.rb +2 -1
  135. data/lib/active_support/i18n_railtie.rb +21 -14
  136. data/lib/active_support/inflector/inflections.rb +25 -7
  137. data/lib/active_support/inflector/methods.rb +50 -64
  138. data/lib/active_support/inflector/transliterate.rb +4 -2
  139. data/lib/active_support/isolated_execution_state.rb +76 -0
  140. data/lib/active_support/json/decoding.rb +2 -1
  141. data/lib/active_support/json/encoding.rb +27 -45
  142. data/lib/active_support/key_generator.rb +31 -6
  143. data/lib/active_support/lazy_load_hooks.rb +33 -7
  144. data/lib/active_support/locale/en.yml +4 -2
  145. data/lib/active_support/log_subscriber/test_helper.rb +2 -2
  146. data/lib/active_support/log_subscriber.rb +97 -35
  147. data/lib/active_support/logger.rb +9 -60
  148. data/lib/active_support/logger_thread_safe_level.rb +11 -34
  149. data/lib/active_support/message_encryptor.rb +206 -56
  150. data/lib/active_support/message_encryptors.rb +141 -0
  151. data/lib/active_support/message_pack/cache_serializer.rb +23 -0
  152. data/lib/active_support/message_pack/extensions.rb +292 -0
  153. data/lib/active_support/message_pack/serializer.rb +63 -0
  154. data/lib/active_support/message_pack.rb +50 -0
  155. data/lib/active_support/message_verifier.rb +235 -84
  156. data/lib/active_support/message_verifiers.rb +135 -0
  157. data/lib/active_support/messages/codec.rb +65 -0
  158. data/lib/active_support/messages/metadata.rb +112 -46
  159. data/lib/active_support/messages/rotation_coordinator.rb +93 -0
  160. data/lib/active_support/messages/rotator.rb +34 -32
  161. data/lib/active_support/messages/serializer_with_fallback.rb +158 -0
  162. data/lib/active_support/multibyte/chars.rb +12 -11
  163. data/lib/active_support/multibyte/unicode.rb +9 -49
  164. data/lib/active_support/multibyte.rb +1 -1
  165. data/lib/active_support/notifications/fanout.rb +304 -114
  166. data/lib/active_support/notifications/instrumenter.rb +117 -35
  167. data/lib/active_support/notifications.rb +25 -25
  168. data/lib/active_support/number_helper/number_converter.rb +14 -7
  169. data/lib/active_support/number_helper/number_to_currency_converter.rb +11 -6
  170. data/lib/active_support/number_helper/number_to_delimited_converter.rb +1 -1
  171. data/lib/active_support/number_helper/number_to_human_size_converter.rb +4 -4
  172. data/lib/active_support/number_helper/number_to_phone_converter.rb +2 -1
  173. data/lib/active_support/number_helper/number_to_rounded_converter.rb +10 -6
  174. data/lib/active_support/number_helper/rounding_helper.rb +2 -6
  175. data/lib/active_support/number_helper.rb +379 -319
  176. data/lib/active_support/option_merger.rb +10 -18
  177. data/lib/active_support/ordered_hash.rb +4 -4
  178. data/lib/active_support/ordered_options.rb +15 -1
  179. data/lib/active_support/parameter_filter.rb +105 -81
  180. data/lib/active_support/proxy_object.rb +2 -0
  181. data/lib/active_support/railtie.rb +83 -21
  182. data/lib/active_support/reloader.rb +13 -5
  183. data/lib/active_support/rescuable.rb +18 -16
  184. data/lib/active_support/ruby_features.rb +7 -0
  185. data/lib/active_support/secure_compare_rotator.rb +18 -11
  186. data/lib/active_support/security_utils.rb +1 -1
  187. data/lib/active_support/string_inquirer.rb +3 -3
  188. data/lib/active_support/subscriber.rb +11 -40
  189. data/lib/active_support/syntax_error_proxy.rb +60 -0
  190. data/lib/active_support/tagged_logging.rb +65 -25
  191. data/lib/active_support/test_case.rb +166 -27
  192. data/lib/active_support/testing/assertions.rb +61 -15
  193. data/lib/active_support/testing/autorun.rb +0 -2
  194. data/lib/active_support/testing/constant_stubbing.rb +32 -0
  195. data/lib/active_support/testing/deprecation.rb +53 -2
  196. data/lib/active_support/testing/error_reporter_assertions.rb +107 -0
  197. data/lib/active_support/testing/isolation.rb +30 -29
  198. data/lib/active_support/testing/method_call_assertions.rb +24 -11
  199. data/lib/active_support/testing/parallelization/server.rb +4 -0
  200. data/lib/active_support/testing/parallelization/worker.rb +3 -0
  201. data/lib/active_support/testing/parallelization.rb +4 -0
  202. data/lib/active_support/testing/parallelize_executor.rb +81 -0
  203. data/lib/active_support/testing/setup_and_teardown.rb +2 -0
  204. data/lib/active_support/testing/stream.rb +4 -6
  205. data/lib/active_support/testing/strict_warnings.rb +39 -0
  206. data/lib/active_support/testing/tagged_logging.rb +1 -1
  207. data/lib/active_support/testing/time_helpers.rb +49 -16
  208. data/lib/active_support/time_with_zone.rb +39 -28
  209. data/lib/active_support/values/time_zone.rb +50 -18
  210. data/lib/active_support/version.rb +1 -1
  211. data/lib/active_support/xml_mini/jdom.rb +4 -11
  212. data/lib/active_support/xml_mini/libxml.rb +5 -5
  213. data/lib/active_support/xml_mini/libxmlsax.rb +1 -1
  214. data/lib/active_support/xml_mini/nokogiri.rb +5 -5
  215. data/lib/active_support/xml_mini/nokogirisax.rb +2 -2
  216. data/lib/active_support/xml_mini/rexml.rb +2 -2
  217. data/lib/active_support/xml_mini.rb +7 -6
  218. data/lib/active_support.rb +28 -1
  219. metadata +150 -18
  220. data/lib/active_support/core_ext/marshal.rb +0 -26
  221. data/lib/active_support/core_ext/range/include_time_with_zone.rb +0 -28
  222. data/lib/active_support/core_ext/range/overlaps.rb +0 -10
  223. data/lib/active_support/core_ext/uri.rb +0 -29
  224. data/lib/active_support/dependencies/zeitwerk_integration.rb +0 -117
  225. 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,35 +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, coder: DEFAULT_CODER, 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
- coder: coder
182
- end
183
-
184
- def redis
185
- @redis ||= begin
186
- pool_options = self.class.send(:retrieve_pool_options, redis_options)
187
-
188
- if pool_options.any?
189
- self.class.send(:ensure_connection_pool_added!)
190
- ::ConnectionPool.new(pool_options) { self.class.build_redis(**redis_options) }
191
- else
192
- self.class.build_redis(**redis_options)
193
- end
194
- end
161
+ super(universal_options)
195
162
  end
196
163
 
197
164
  def inspect
198
- instance = @redis || @redis_options
199
- "#<#{self.class} options=#{options.inspect} redis=#{instance.inspect}>"
165
+ "#<#{self.class} options=#{options.inspect} redis=#{redis.inspect}>"
200
166
  end
201
167
 
202
168
  # Cache Store API implementation.
@@ -204,14 +170,13 @@ module ActiveSupport
204
170
  # Read multiple values at once. Returns a hash of requested keys ->
205
171
  # fetched values.
206
172
  def read_multi(*names)
207
- if mget_capable?
208
- instrument(:read_multi, names, options) do |payload|
209
- read_multi_mget(*names).tap do |results|
210
- payload[:hits] = results.keys
211
- 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
212
179
  end
213
- else
214
- super
215
180
  end
216
181
  end
217
182
 
@@ -235,7 +200,7 @@ module ActiveSupport
235
200
  unless String === matcher
236
201
  raise ArgumentError, "Only Redis glob strings are supported: #{matcher.inspect}"
237
202
  end
238
- redis.with do |c|
203
+ redis.then do |c|
239
204
  pattern = namespace_key(matcher, options)
240
205
  cursor = "0"
241
206
  # Fetch keys in batches using SCAN to avoid blocking the Redis server.
@@ -251,12 +216,21 @@ module ActiveSupport
251
216
  end
252
217
  end
253
218
 
254
- # 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
255
231
  #
256
- # Increment a cached value. This method uses the Redis incr 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.
232
+ # Incrementing a non-numeric value, or a value written without
233
+ # <tt>raw: true</tt>, will fail and return +nil+.
260
234
  #
261
235
  # Failsafe: Raises errors.
262
236
  def increment(name, amount = 1, options = nil)
@@ -264,22 +238,25 @@ module ActiveSupport
264
238
  failsafe :increment do
265
239
  options = merged_options(options)
266
240
  key = normalize_key(name, options)
267
-
268
- redis.with do |c|
269
- c.incrby(key, amount).tap do
270
- write_key_expiry(c, key, options)
271
- end
272
- end
241
+ change_counter(key, amount, options)
273
242
  end
274
243
  end
275
244
  end
276
245
 
277
- # 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+:
278
250
  #
279
- # Decrement a cached value. This method uses the Redis decr atomic
280
- # operator and can only be used on values written with the :raw option.
281
- # Calling it on a value not stored with :raw will initialize that value
282
- # 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+.
283
260
  #
284
261
  # Failsafe: Raises errors.
285
262
  def decrement(name, amount = 1, options = nil)
@@ -287,12 +264,7 @@ module ActiveSupport
287
264
  failsafe :decrement do
288
265
  options = merged_options(options)
289
266
  key = normalize_key(name, options)
290
-
291
- redis.with do |c|
292
- c.decrby(key, amount).tap do
293
- write_key_expiry(c, key, options)
294
- end
295
- end
267
+ change_counter(key, -amount, options)
296
268
  end
297
269
  end
298
270
  end
@@ -314,67 +286,60 @@ module ActiveSupport
314
286
  if namespace = merged_options(options)[:namespace]
315
287
  delete_matched "*", namespace: namespace
316
288
  else
317
- redis.with { |c| c.flushdb }
289
+ redis.then { |c| c.flushdb }
318
290
  end
319
291
  end
320
292
  end
321
293
 
322
- def mget_capable? #:nodoc:
323
- set_redis_capabilities unless defined? @mget_capable
324
- @mget_capable
325
- end
326
-
327
- def mset_capable? #:nodoc:
328
- set_redis_capabilities unless defined? @mset_capable
329
- @mset_capable
294
+ # Get info from redis servers.
295
+ def stats
296
+ redis.then { |c| c.info }
330
297
  end
331
298
 
332
299
  private
333
- def set_redis_capabilities
334
- case redis
335
- when Redis::Distributed
336
- @mget_capable = true
337
- @mset_capable = false
338
- else
339
- @mget_capable = true
340
- @mset_capable = true
341
- 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
+ }
342
310
  end
343
311
 
344
312
  # Store provider interface:
345
313
  # Read an entry from the cache.
346
314
  def read_entry(key, **options)
347
- failsafe :read_entry do
348
- raw = options&.fetch(:raw, false)
349
- deserialize_entry(redis.with { |c| c.get(key) }, raw: raw)
350
- end
315
+ deserialize_entry(read_serialized_entry(key, **options), **options)
351
316
  end
352
317
 
353
- def read_multi_entries(names, **options)
354
- if mget_capable?
355
- read_multi_mget(*names, **options)
356
- else
357
- super
318
+ def read_serialized_entry(key, raw: false, **options)
319
+ failsafe :read_entry do
320
+ redis.then { |c| c.get(key) }
358
321
  end
359
322
  end
360
323
 
361
- def read_multi_mget(*names)
362
- options = names.extract_options!
324
+ def read_multi_entries(names, **options)
363
325
  options = merged_options(options)
364
326
  return {} if names == []
365
327
  raw = options&.fetch(:raw, false)
366
328
 
367
329
  keys = names.map { |name| normalize_key(name, options) }
368
330
 
369
- values = failsafe(:read_multi_mget, returning: {}) do
370
- redis.with { |c| c.mget(*keys) }
331
+ values = failsafe(:read_multi_entries, returning: {}) do
332
+ redis.then { |c| c.mget(*keys) }
371
333
  end
372
334
 
373
335
  names.zip(values).each_with_object({}) do |(name, value), results|
374
336
  if value
375
337
  entry = deserialize_entry(value, raw: raw)
376
338
  unless entry.nil? || entry.expired? || entry.mismatched?(normalize_version(name, options))
377
- results[name] = entry.value
339
+ begin
340
+ results[name] = entry.value
341
+ rescue DeserializationError
342
+ end
378
343
  end
379
344
  end
380
345
  end
@@ -383,9 +348,11 @@ module ActiveSupport
383
348
  # Write an entry to the cache.
384
349
  #
385
350
  # Requires Redis 2.6.12+ for extended SET options.
386
- def write_entry(key, entry, unless_exist: false, raw: false, expires_in: nil, race_condition_ttl: nil, **options)
387
- 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
388
354
 
355
+ def write_serialized_entry(key, payload, raw: false, unless_exist: false, expires_in: nil, race_condition_ttl: nil, pipeline: nil, **options)
389
356
  # If race condition TTL is in use, ensure that cache entries
390
357
  # stick around a bit longer after they would have expired
391
358
  # so we can purposefully serve stale entries.
@@ -393,46 +360,46 @@ module ActiveSupport
393
360
  expires_in += 5.minutes
394
361
  end
395
362
 
396
- failsafe :write_entry, returning: false do
397
- if unless_exist || expires_in
398
- modifiers = {}
399
- modifiers[:nx] = unless_exist
400
- modifiers[:px] = (1000 * expires_in.to_f).ceil if expires_in
401
-
402
- redis.with { |c| c.set key, serialized_entry, **modifiers }
403
- else
404
- redis.with { |c| c.set key, serialized_entry }
405
- end
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
406
367
  end
407
- end
408
368
 
409
- def write_key_expiry(client, key, options)
410
- if options[:expires_in] && client.ttl(key).negative?
411
- 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
412
375
  end
413
376
  end
414
377
 
415
378
  # Delete an entry from the cache.
416
- def delete_entry(key, options)
379
+ def delete_entry(key, **options)
417
380
  failsafe :delete_entry, returning: false do
418
- redis.with { |c| c.del key }
381
+ redis.then { |c| c.del(key) == 1 }
419
382
  end
420
383
  end
421
384
 
422
385
  # Deletes multiple entries in the cache. Returns the number of entries deleted.
423
386
  def delete_multi_entries(entries, **_options)
424
- redis.with { |c| c.del(entries) }
387
+ failsafe :delete_multi_entries, returning: 0 do
388
+ redis.then { |c| c.del(entries) }
389
+ end
425
390
  end
426
391
 
427
392
  # Nonstandard store provider API to write multiple values at once.
428
- def write_multi_entries(entries, expires_in: nil, **options)
429
- if entries.any?
430
- if mset_capable? && expires_in.nil?
431
- failsafe :write_multi_entries do
432
- 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
433
402
  end
434
- else
435
- super
436
403
  end
437
404
  end
438
405
  end
@@ -444,7 +411,7 @@ module ActiveSupport
444
411
 
445
412
  def truncate_key(key)
446
413
  if key && key.bytesize > max_key_bytesize
447
- suffix = ":sha2:#{::Digest::SHA2.hexdigest(key)}"
414
+ suffix = ":hash:#{ActiveSupport::Digest.hexdigest(key)}"
448
415
  truncate_at = max_key_bytesize - suffix.bytesize
449
416
  "#{key.byteslice(0, truncate_at)}#{suffix}"
450
417
  else
@@ -452,42 +419,68 @@ module ActiveSupport
452
419
  end
453
420
  end
454
421
 
455
- def deserialize_entry(payload, raw:)
456
- if payload && raw
457
- Entry.new(payload, compress: false)
422
+ def deserialize_entry(payload, raw: false, **)
423
+ if raw && !payload.nil?
424
+ Entry.new(payload)
458
425
  else
459
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
- super(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