activesupport 7.2.3.1 → 8.1.4.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 (135) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +584 -234
  3. data/lib/active_support/backtrace_cleaner.rb +72 -1
  4. data/lib/active_support/benchmark.rb +21 -0
  5. data/lib/active_support/benchmarkable.rb +3 -2
  6. data/lib/active_support/cache/file_store.rb +13 -2
  7. data/lib/active_support/cache/mem_cache_store.rb +17 -14
  8. data/lib/active_support/cache/memory_store.rb +9 -3
  9. data/lib/active_support/cache/redis_cache_store.rb +51 -34
  10. data/lib/active_support/cache/strategy/local_cache.rb +17 -8
  11. data/lib/active_support/cache/strategy/local_cache_middleware.rb +7 -7
  12. data/lib/active_support/cache.rb +86 -18
  13. data/lib/active_support/callbacks.rb +25 -11
  14. data/lib/active_support/class_attribute.rb +33 -0
  15. data/lib/active_support/code_generator.rb +9 -0
  16. data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +8 -62
  17. data/lib/active_support/concurrency/share_lock.rb +0 -1
  18. data/lib/active_support/concurrency/thread_monitor.rb +55 -0
  19. data/lib/active_support/configurable.rb +34 -0
  20. data/lib/active_support/configuration_file.rb +15 -6
  21. data/lib/active_support/continuous_integration.rb +145 -0
  22. data/lib/active_support/core_ext/array/conversions.rb +3 -3
  23. data/lib/active_support/core_ext/array.rb +7 -7
  24. data/lib/active_support/core_ext/benchmark.rb +0 -16
  25. data/lib/active_support/core_ext/big_decimal.rb +1 -1
  26. data/lib/active_support/core_ext/class/attribute.rb +24 -18
  27. data/lib/active_support/core_ext/class.rb +2 -2
  28. data/lib/active_support/core_ext/date/conversions.rb +2 -0
  29. data/lib/active_support/core_ext/date.rb +5 -5
  30. data/lib/active_support/core_ext/date_and_time/compatibility.rb +0 -35
  31. data/lib/active_support/core_ext/date_time/calculations.rb +1 -0
  32. data/lib/active_support/core_ext/date_time/compatibility.rb +3 -5
  33. data/lib/active_support/core_ext/date_time.rb +5 -5
  34. data/lib/active_support/core_ext/digest.rb +1 -1
  35. data/lib/active_support/core_ext/enumerable.rb +8 -3
  36. data/lib/active_support/core_ext/erb/util.rb +3 -3
  37. data/lib/active_support/core_ext/file.rb +1 -1
  38. data/lib/active_support/core_ext/hash/deep_merge.rb +1 -0
  39. data/lib/active_support/core_ext/hash/except.rb +0 -12
  40. data/lib/active_support/core_ext/hash.rb +8 -8
  41. data/lib/active_support/core_ext/integer.rb +3 -3
  42. data/lib/active_support/core_ext/kernel.rb +3 -3
  43. data/lib/active_support/core_ext/module/attr_internal.rb +3 -4
  44. data/lib/active_support/core_ext/module.rb +11 -11
  45. data/lib/active_support/core_ext/numeric.rb +3 -3
  46. data/lib/active_support/core_ext/object/instance_variables.rb +1 -1
  47. data/lib/active_support/core_ext/object/json.rb +24 -11
  48. data/lib/active_support/core_ext/object/to_query.rb +7 -1
  49. data/lib/active_support/core_ext/object.rb +13 -13
  50. data/lib/active_support/core_ext/pathname.rb +2 -2
  51. data/lib/active_support/core_ext/range/compare_range.rb +2 -14
  52. data/lib/active_support/core_ext/range/overlap.rb +3 -3
  53. data/lib/active_support/core_ext/range/sole.rb +2 -0
  54. data/lib/active_support/core_ext/range.rb +4 -5
  55. data/lib/active_support/core_ext/string/filters.rb +2 -0
  56. data/lib/active_support/core_ext/string/inflections.rb +1 -1
  57. data/lib/active_support/core_ext/string/multibyte.rb +10 -1
  58. data/lib/active_support/core_ext/string/output_safety.rb +27 -13
  59. data/lib/active_support/core_ext/string.rb +13 -13
  60. data/lib/active_support/core_ext/symbol.rb +1 -1
  61. data/lib/active_support/core_ext/thread/backtrace/location.rb +2 -7
  62. data/lib/active_support/core_ext/time/calculations.rb +11 -2
  63. data/lib/active_support/core_ext/time/compatibility.rb +2 -27
  64. data/lib/active_support/core_ext/time/conversions.rb +2 -0
  65. data/lib/active_support/core_ext/time.rb +5 -5
  66. data/lib/active_support/current_attributes/test_helper.rb +2 -2
  67. data/lib/active_support/current_attributes.rb +13 -10
  68. data/lib/active_support/delegation.rb +25 -44
  69. data/lib/active_support/dependencies/interlock.rb +11 -5
  70. data/lib/active_support/dependencies.rb +6 -2
  71. data/lib/active_support/deprecation/reporting.rb +4 -21
  72. data/lib/active_support/deprecation.rb +1 -1
  73. data/lib/active_support/duration.rb +20 -16
  74. data/lib/active_support/editor.rb +70 -0
  75. data/lib/active_support/encrypted_configuration.rb +20 -2
  76. data/lib/active_support/error_reporter.rb +76 -2
  77. data/lib/active_support/event_reporter/test_helper.rb +32 -0
  78. data/lib/active_support/event_reporter.rb +592 -0
  79. data/lib/active_support/evented_file_update_checker.rb +5 -2
  80. data/lib/active_support/execution_context.rb +75 -7
  81. data/lib/active_support/file_update_checker.rb +7 -5
  82. data/lib/active_support/gem_version.rb +3 -3
  83. data/lib/active_support/gzip.rb +1 -0
  84. data/lib/active_support/hash_with_indifferent_access.rb +44 -24
  85. data/lib/active_support/i18n_railtie.rb +19 -11
  86. data/lib/active_support/inflector/inflections.rb +34 -16
  87. data/lib/active_support/inflector/methods.rb +6 -22
  88. data/lib/active_support/inflector/transliterate.rb +18 -10
  89. data/lib/active_support/isolated_execution_state.rb +17 -17
  90. data/lib/active_support/json/decoding.rb +9 -3
  91. data/lib/active_support/json/encoding.rb +149 -19
  92. data/lib/active_support/log_subscriber.rb +2 -6
  93. data/lib/active_support/logger_thread_safe_level.rb +6 -3
  94. data/lib/active_support/message_encryptors.rb +52 -0
  95. data/lib/active_support/message_pack/extensions.rb +23 -1
  96. data/lib/active_support/message_verifiers.rb +52 -0
  97. data/lib/active_support/messages/rotation_coordinator.rb +9 -0
  98. data/lib/active_support/messages/rotator.rb +5 -0
  99. data/lib/active_support/messages/serializer_with_fallback.rb +1 -1
  100. data/lib/active_support/multibyte/chars.rb +8 -1
  101. data/lib/active_support/multibyte.rb +4 -0
  102. data/lib/active_support/notifications/fanout.rb +64 -43
  103. data/lib/active_support/notifications/instrumenter.rb +1 -1
  104. data/lib/active_support/notifications.rb +2 -2
  105. data/lib/active_support/number_helper/number_converter.rb +4 -1
  106. data/lib/active_support/number_helper/number_to_currency_converter.rb +1 -1
  107. data/lib/active_support/number_helper/number_to_delimited_converter.rb +3 -4
  108. data/lib/active_support/number_helper/number_to_phone_converter.rb +3 -7
  109. data/lib/active_support/number_helper/rounding_helper.rb +1 -1
  110. data/lib/active_support/number_helper.rb +22 -0
  111. data/lib/active_support/ordered_options.rb +2 -2
  112. data/lib/active_support/railtie.rb +32 -9
  113. data/lib/active_support/security_utils.rb +4 -5
  114. data/lib/active_support/structured_event_subscriber.rb +99 -0
  115. data/lib/active_support/subscriber.rb +0 -5
  116. data/lib/active_support/syntax_error_proxy.rb +9 -1
  117. data/lib/active_support/tagged_logging.rb +5 -0
  118. data/lib/active_support/test_case.rb +67 -6
  119. data/lib/active_support/testing/assertions.rb +118 -27
  120. data/lib/active_support/testing/autorun.rb +5 -0
  121. data/lib/active_support/testing/error_reporter_assertions.rb +17 -0
  122. data/lib/active_support/testing/event_reporter_assertions.rb +227 -0
  123. data/lib/active_support/testing/isolation.rb +0 -2
  124. data/lib/active_support/testing/notification_assertions.rb +92 -0
  125. data/lib/active_support/testing/parallelization/worker.rb +7 -1
  126. data/lib/active_support/testing/parallelization.rb +17 -0
  127. data/lib/active_support/testing/tests_without_assertions.rb +1 -1
  128. data/lib/active_support/testing/time_helpers.rb +9 -4
  129. data/lib/active_support/time_with_zone.rb +36 -23
  130. data/lib/active_support/values/time_zone.rb +22 -13
  131. data/lib/active_support/xml_mini.rb +1 -2
  132. data/lib/active_support.rb +21 -9
  133. metadata +33 -17
  134. data/lib/active_support/core_ext/range/each.rb +0 -24
  135. data/lib/active_support/proxy_object.rb +0 -20
@@ -18,7 +18,7 @@ module ActiveSupport
18
18
  #
19
19
  # bc = ActiveSupport::BacktraceCleaner.new
20
20
  # root = "#{Rails.root}/"
21
- # bc.add_filter { |line| line.start_with?(root) ? line.from(root.size) : line } # strip the Rails.root prefix
21
+ # bc.add_filter { |line| line.delete_prefix(root) } # strip the Rails.root prefix
22
22
  # bc.add_silencer { |line| /puma|rubygems/.match?(line) } # skip any lines from puma or rubygems
23
23
  # bc.clean(exception.backtrace) # perform the cleanup
24
24
  #
@@ -56,6 +56,18 @@ module ActiveSupport
56
56
  end
57
57
  alias :filter :clean
58
58
 
59
+ # Given an array of Thread::Backtrace::Location objects, returns an array
60
+ # with the clean ones:
61
+ #
62
+ # clean_locations = backtrace_cleaner.clean_locations(caller_locations)
63
+ #
64
+ # Filters and silencers receive strings as usual. However, the +path+
65
+ # attributes of the locations in the returned array are the original,
66
+ # unfiltered ones, since locations are immutable.
67
+ def clean_locations(locations, kind = :silent)
68
+ locations.select { |location| clean_frame(location, kind) }
69
+ end
70
+
59
71
  # Returns the frame with all filters applied.
60
72
  # returns +nil+ if the frame was silenced.
61
73
  def clean_frame(frame, kind = :silent)
@@ -74,6 +86,65 @@ module ActiveSupport
74
86
  end
75
87
  end
76
88
 
89
+ # Thread.each_caller_location does not accept a start in Ruby < 3.4.
90
+ if Thread.method(:each_caller_location).arity == 0
91
+ # Returns the first clean frame of the caller's backtrace, or +nil+.
92
+ #
93
+ # Frames are strings.
94
+ def first_clean_frame(kind = :silent)
95
+ caller_location_skipped = false
96
+
97
+ Thread.each_caller_location do |location|
98
+ unless caller_location_skipped
99
+ caller_location_skipped = true
100
+ next
101
+ end
102
+
103
+ frame = clean_frame(location, kind)
104
+ return frame if frame
105
+ end
106
+ end
107
+
108
+ # Returns the first clean location of the caller's call stack, or +nil+.
109
+ #
110
+ # Locations are Thread::Backtrace::Location objects. Since they are
111
+ # immutable, their +path+ attributes are the original ones, but filters
112
+ # are applied internally so silencers can still rely on them.
113
+ def first_clean_location(kind = :silent)
114
+ caller_location_skipped = false
115
+
116
+ Thread.each_caller_location do |location|
117
+ unless caller_location_skipped
118
+ caller_location_skipped = true
119
+ next
120
+ end
121
+
122
+ return location if clean_frame(location, kind)
123
+ end
124
+ end
125
+ else
126
+ # Returns the first clean frame of the caller's backtrace, or +nil+.
127
+ #
128
+ # Frames are strings.
129
+ def first_clean_frame(kind = :silent)
130
+ Thread.each_caller_location(2) do |location|
131
+ frame = clean_frame(location, kind)
132
+ return frame if frame
133
+ end
134
+ end
135
+
136
+ # Returns the first clean location of the caller's call stack, or +nil+.
137
+ #
138
+ # Locations are Thread::Backtrace::Location objects. Since they are
139
+ # immutable, their +path+ attributes are the original ones, but filters
140
+ # are applied internally so silencers can still rely on them.
141
+ def first_clean_location(kind = :silent)
142
+ Thread.each_caller_location(2) do |location|
143
+ return location if clean_frame(location, kind)
144
+ end
145
+ end
146
+ end
147
+
77
148
  # Adds a filter from the block provided. Each line in the backtrace will be
78
149
  # mapped against this filter.
79
150
  #
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveSupport
4
+ module Benchmark # :nodoc:
5
+ # Benchmark realtime in the specified time unit. By default,
6
+ # the returned unit is in seconds.
7
+ #
8
+ # ActiveSupport::Benchmark.realtime { sleep 0.1 }
9
+ # # => 0.10007
10
+ #
11
+ # ActiveSupport::Benchmark.realtime(:float_millisecond) { sleep 0.1 }
12
+ # # => 100.07
13
+ #
14
+ # `unit` can be any of the values accepted by Ruby's `Process.clock_gettime`.
15
+ def self.realtime(unit = :float_second, &block)
16
+ time_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, unit)
17
+ yield
18
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, unit) - time_start
19
+ end
20
+ end
21
+ end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/core_ext/benchmark"
4
3
  require "active_support/core_ext/hash/keys"
5
4
 
6
5
  module ActiveSupport
@@ -41,7 +40,9 @@ module ActiveSupport
41
40
  options[:level] ||= :info
42
41
 
43
42
  result = nil
44
- ms = Benchmark.ms { result = options[:silence] ? logger.silence(&block) : yield }
43
+ ms = ActiveSupport::Benchmark.realtime(:float_millisecond) do
44
+ result = options[:silence] ? logger.silence(&block) : yield
45
+ end
45
46
  logger.public_send(options[:level], "%s (%.1fms)" % [ message, ms ])
46
47
  result
47
48
  else
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "fileutils"
3
4
  require "active_support/core_ext/file/atomic"
4
5
  require "active_support/core_ext/string/conversions"
5
6
  require "uri/common"
@@ -58,7 +59,12 @@ module ActiveSupport
58
59
  # cache.increment("baz") # => 6
59
60
  #
60
61
  def increment(name, amount = 1, **options)
61
- modify_value(name, amount, options)
62
+ options = merged_options(options)
63
+ key = normalize_key(name, options)
64
+
65
+ instrument(:increment, key, amount: amount) do
66
+ modify_value(name, amount, options)
67
+ end
62
68
  end
63
69
 
64
70
  # Decrement a cached integer value. Returns the updated value.
@@ -73,7 +79,12 @@ module ActiveSupport
73
79
  # cache.decrement("baz") # => 4
74
80
  #
75
81
  def decrement(name, amount = 1, **options)
76
- modify_value(name, -amount, options)
82
+ options = merged_options(options)
83
+ key = normalize_key(name, options)
84
+
85
+ instrument(:decrement, key, amount: amount) do
86
+ modify_value(name, -amount, options)
87
+ end
77
88
  end
78
89
 
79
90
  def delete_matched(matcher, options = nil)
@@ -41,7 +41,6 @@ module ActiveSupport
41
41
 
42
42
  prepend Strategy::LocalCache
43
43
 
44
- KEY_MAX_SIZE = 250
45
44
  ESCAPE_KEY_CHARS = /[\x00-\x20%\x7F-\xFF]/n
46
45
 
47
46
  # Creates a new Dalli::Client instance with specified addresses and options.
@@ -60,7 +59,7 @@ module ActiveSupport
60
59
  pool_options = retrieve_pool_options(options)
61
60
 
62
61
  if pool_options
63
- ConnectionPool.new(pool_options) { Dalli::Client.new(addresses, options.merge(threadsafe: false)) }
62
+ ConnectionPool.new(**pool_options) { Dalli::Client.new(addresses, options.merge(threadsafe: false)) }
64
63
  else
65
64
  Dalli::Client.new(addresses, options)
66
65
  end
@@ -80,6 +79,7 @@ module ActiveSupport
80
79
  if options.key?(:cache_nils)
81
80
  options[:skip_nil] = !options.delete(:cache_nils)
82
81
  end
82
+ options[:max_key_size] ||= MAX_KEY_SIZE
83
83
  super(options)
84
84
 
85
85
  unless [String, Dalli::Client, NilClass].include?(addresses.first.class)
@@ -90,6 +90,9 @@ module ActiveSupport
90
90
  # The value "compress: false" prevents duplicate compression within Dalli.
91
91
  @mem_cache_options[:compress] = false
92
92
  (OVERRIDDEN_OPTIONS - %i(compress)).each { |name| @mem_cache_options.delete(name) }
93
+ # Set the default serializer for Dalli to prevent warning about
94
+ # inheriting the default serializer.
95
+ @mem_cache_options[:serializer] = Marshal
93
96
  @data = self.class.build_mem_cache(*(addresses + [@mem_cache_options]))
94
97
  end
95
98
 
@@ -110,9 +113,6 @@ module ActiveSupport
110
113
  # * <tt>raw: true</tt> - Sends the value directly to the server as raw
111
114
  # bytes. The value must be a string or number. You can use memcached
112
115
  # direct operations like +increment+ and +decrement+ only on raw values.
113
- #
114
- # * <tt>unless_exist: true</tt> - Prevents overwriting an existing cache
115
- # entry.
116
116
 
117
117
  # Increment a cached integer value using the memcached incr atomic operator.
118
118
  # Returns the updated value.
@@ -129,6 +129,11 @@ module ActiveSupport
129
129
  #
130
130
  # Incrementing a non-numeric value, or a value written without
131
131
  # <tt>raw: true</tt>, will fail and return +nil+.
132
+ #
133
+ # To read the value later, call #read_counter:
134
+ #
135
+ # cache.increment("baz") # => 7
136
+ # cache.read_counter("baz") # 7
132
137
  def increment(name, amount = 1, options = nil)
133
138
  options = merged_options(options)
134
139
  key = normalize_key(name, options)
@@ -155,6 +160,11 @@ module ActiveSupport
155
160
  #
156
161
  # Decrementing a non-numeric value, or a value written without
157
162
  # <tt>raw: true</tt>, will fail and return +nil+.
163
+ #
164
+ # To read the value later, call #read_counter:
165
+ #
166
+ # cache.decrement("baz") # => 3
167
+ # cache.read_counter("baz") # 3
158
168
  def decrement(name, amount = 1, options = nil)
159
169
  options = merged_options(options)
160
170
  key = normalize_key(name, options)
@@ -249,19 +259,12 @@ module ActiveSupport
249
259
  # before applying the regular expression to ensure we are escaping all
250
260
  # characters properly.
251
261
  def normalize_key(key, options)
252
- key = super
262
+ key = expand_and_namespace_key(key, options)
253
263
  if key
254
264
  key = key.dup.force_encoding(Encoding::ASCII_8BIT)
255
265
  key = key.gsub(ESCAPE_KEY_CHARS) { |match| "%#{match.getbyte(0).to_s(16).upcase}" }
256
-
257
- if key.size > KEY_MAX_SIZE
258
- key_separator = ":hash:"
259
- key_hash = ActiveSupport::Digest.hexdigest(key)
260
- key_trim_size = KEY_MAX_SIZE - key_separator.size - key_hash.size
261
- key = "#{key[0, key_trim_size]}#{key_separator}#{key_hash}"
262
- end
263
266
  end
264
- key
267
+ truncate_key(key)
265
268
  end
266
269
 
267
270
  def deserialize_entry(payload, raw: false, **)
@@ -26,6 +26,8 @@ module ActiveSupport
26
26
  #
27
27
  # +MemoryStore+ is thread-safe.
28
28
  class MemoryStore < Store
29
+ prepend Strategy::LocalCache
30
+
29
31
  module DupCoder # :nodoc:
30
32
  extend self
31
33
 
@@ -103,7 +105,7 @@ module ActiveSupport
103
105
  _instrument(:cleanup, size: @data.size) do
104
106
  keys = synchronize { @data.keys }
105
107
  keys.each do |key|
106
- entry = @data[key]
108
+ entry = deserialize_entry(@data[key])
107
109
  delete_entry(key, **options) if entry && entry.expired?
108
110
  end
109
111
  end
@@ -147,7 +149,9 @@ module ActiveSupport
147
149
  # cache.increment("baz") # => 6
148
150
  #
149
151
  def increment(name, amount = 1, **options)
150
- modify_value(name, amount, **options)
152
+ instrument(:increment, name, amount: amount) do
153
+ modify_value(name, amount, **options)
154
+ end
151
155
  end
152
156
 
153
157
  # Decrement a cached integer value. Returns the updated value.
@@ -162,7 +166,9 @@ module ActiveSupport
162
166
  # cache.decrement("baz") # => 4
163
167
  #
164
168
  def decrement(name, amount = 1, **options)
165
- modify_value(name, -amount, **options)
169
+ instrument(:decrement, name, amount: amount) do
170
+ modify_value(name, -amount, **options)
171
+ end
166
172
  end
167
173
 
168
174
  # Deletes cache entries if the cache key matches a given pattern.
@@ -35,9 +35,6 @@ module ActiveSupport
35
35
  # +Redis::Distributed+ 4.0.1+ for distributed mget support.
36
36
  # * +delete_matched+ support for Redis KEYS globs.
37
37
  class RedisCacheStore < Store
38
- # Keys are truncated with the Active Support digest if they exceed 1kB
39
- MAX_KEY_BYTESIZE = 1024
40
-
41
38
  DEFAULT_REDIS_OPTIONS = {
42
39
  connect_timeout: 1,
43
40
  read_timeout: 1,
@@ -106,20 +103,29 @@ module ActiveSupport
106
103
  end
107
104
  end
108
105
 
109
- attr_reader :max_key_bytesize
110
106
  attr_reader :redis
111
107
 
112
108
  # Creates a new Redis cache store.
113
109
  #
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.
110
+ # There are a few ways to provide the Redis client used by the cache:
111
+ #
112
+ # 1. The +:redis+ param can be:
113
+ # - A Redis instance.
114
+ # - A +ConnectionPool+ instance wrapping a Redis instance.
115
+ # - A block that returns a Redis instance.
116
+ #
117
+ # 2. The +:url+ param can be:
118
+ # - A string used to create a Redis instance.
119
+ # - An array of strings used to create a +Redis::Distributed+ instance.
120
+ #
121
+ # If the final Redis instance is not already a +ConnectionPool+, it will
122
+ # be wrapped in one using +ActiveSupport::Cache::Store::DEFAULT_POOL_OPTIONS+.
123
+ # These options can be overridden with the +:pool+ param, or the pool can be
124
+ # disabled with +:pool: false+.
119
125
  #
120
126
  # Option Class Result
121
- # :redis Proc -> options[:redis].call
122
127
  # :redis Object -> options[:redis]
128
+ # :redis Proc -> options[:redis].call
123
129
  # :url String -> Redis.new(url: …)
124
130
  # :url Array -> Redis::Distributed.new([{ url: … }, { url: … }, …])
125
131
  #
@@ -148,14 +154,17 @@ module ActiveSupport
148
154
  # cache.exist?('bar') # => false
149
155
  def initialize(error_handler: DEFAULT_ERROR_HANDLER, **redis_options)
150
156
  universal_options = redis_options.extract!(*UNIVERSAL_OPTIONS)
157
+ redis = redis_options[:redis]
151
158
 
152
- if pool_options = self.class.send(:retrieve_pool_options, redis_options)
153
- @redis = ::ConnectionPool.new(pool_options) { self.class.build_redis(**redis_options) }
159
+ already_pool = redis.instance_of?(::ConnectionPool) ||
160
+ (redis.respond_to?(:wrapped_pool) && redis.wrapped_pool.instance_of?(::ConnectionPool))
161
+
162
+ if !already_pool && pool_options = self.class.send(:retrieve_pool_options, redis_options)
163
+ @redis = ::ConnectionPool.new(**pool_options) { self.class.build_redis(**redis_options) }
154
164
  else
155
165
  @redis = self.class.build_redis(**redis_options)
156
166
  end
157
167
 
158
- @max_key_bytesize = MAX_KEY_BYTESIZE
159
168
  @error_handler = error_handler
160
169
 
161
170
  super(universal_options)
@@ -173,9 +182,12 @@ module ActiveSupport
173
182
  return {} if names.empty?
174
183
 
175
184
  options = names.extract_options!
176
- instrument_multi(:read_multi, names, options) do |payload|
185
+ options = merged_options(options)
186
+ keys = names.map { |name| normalize_key(name, options) }
187
+
188
+ instrument_multi(:read_multi, keys, options) do |payload|
177
189
  read_multi_entries(names, **options).tap do |results|
178
- payload[:hits] = results.keys
190
+ payload[:hits] = results.keys.map { |name| normalize_key(name, options) }
179
191
  end
180
192
  end
181
193
  end
@@ -210,7 +222,7 @@ module ActiveSupport
210
222
  nodes.each do |node|
211
223
  begin
212
224
  cursor, keys = node.scan(cursor, match: pattern, count: SCAN_BATCH_SIZE)
213
- node.del(*keys) unless keys.empty?
225
+ node.unlink(*keys) unless keys.empty?
214
226
  end until cursor == "0"
215
227
  end
216
228
  end
@@ -233,6 +245,11 @@ module ActiveSupport
233
245
  # Incrementing a non-numeric value, or a value written without
234
246
  # <tt>raw: true</tt>, will fail and return +nil+.
235
247
  #
248
+ # To read the value later, call #read_counter:
249
+ #
250
+ # cache.increment("baz") # => 7
251
+ # cache.read_counter("baz") # 7
252
+ #
236
253
  # Failsafe: Raises errors.
237
254
  def increment(name, amount = 1, options = nil)
238
255
  options = merged_options(options)
@@ -260,6 +277,11 @@ module ActiveSupport
260
277
  # Decrementing a non-numeric value, or a value written without
261
278
  # <tt>raw: true</tt>, will fail and return +nil+.
262
279
  #
280
+ # To read the value later, call #read_counter:
281
+ #
282
+ # cache.decrement("baz") # => 3
283
+ # cache.read_counter("baz") # 3
284
+ #
263
285
  # Failsafe: Raises errors.
264
286
  def decrement(name, amount = 1, options = nil)
265
287
  options = merged_options(options)
@@ -381,14 +403,16 @@ module ActiveSupport
381
403
  # Delete an entry from the cache.
382
404
  def delete_entry(key, **options)
383
405
  failsafe :delete_entry, returning: false do
384
- redis.then { |c| c.del(key) == 1 }
406
+ redis.then { |c| c.unlink(key) == 1 }
385
407
  end
386
408
  end
387
409
 
388
410
  # Deletes multiple entries in the cache. Returns the number of entries deleted.
389
411
  def delete_multi_entries(entries, **_options)
412
+ return 0 if entries.empty?
413
+
390
414
  failsafe :delete_multi_entries, returning: 0 do
391
- redis.then { |c| c.del(entries) }
415
+ redis.then { |c| c.unlink(*entries) }
392
416
  end
393
417
  end
394
418
 
@@ -407,21 +431,6 @@ module ActiveSupport
407
431
  end
408
432
  end
409
433
 
410
- # Truncate keys that exceed 1kB.
411
- def normalize_key(key, options)
412
- truncate_key super&.b
413
- end
414
-
415
- def truncate_key(key)
416
- if key && key.bytesize > max_key_bytesize
417
- suffix = ":hash:#{ActiveSupport::Digest.hexdigest(key)}"
418
- truncate_at = max_key_bytesize - suffix.bytesize
419
- "#{key.byteslice(0, truncate_at)}#{suffix}"
420
- else
421
- key
422
- end
423
- end
424
-
425
434
  def deserialize_entry(payload, raw: false, **)
426
435
  if raw && !payload.nil?
427
436
  Entry.new(payload)
@@ -478,9 +487,17 @@ module ActiveSupport
478
487
  @supports_expire_nx = redis_versions.all? { |v| Gem::Version.new(v) >= Gem::Version.new("7.0.0") }
479
488
  end
480
489
 
490
+ FAILSAFE_ERRORS = [
491
+ ::Redis::BaseError,
492
+ ConnectionPool::Error,
493
+ ConnectionPool::TimeoutError,
494
+ (::RedisClient::Error if defined?(::RedisClient::Error)),
495
+ ].compact.freeze
496
+ private_constant :FAILSAFE_ERRORS
497
+
481
498
  def failsafe(method, returning: nil)
482
499
  yield
483
- rescue ::Redis::BaseError, ConnectionPool::Error, ConnectionPool::TimeoutError => error
500
+ rescue *FAILSAFE_ERRORS => error
484
501
  @error_handler&.call(method: method, exception: error, returning: returning)
485
502
  returning
486
503
  end
@@ -68,12 +68,25 @@ module ActiveSupport
68
68
  use_temporary_local_cache(LocalStore.new, &block)
69
69
  end
70
70
 
71
+ # Set a new local cache.
72
+ def new_local_cache
73
+ LocalCacheRegistry.set_cache_for(local_cache_key, LocalStore.new)
74
+ end
75
+
76
+ # Unset the current local cache.
77
+ def unset_local_cache
78
+ LocalCacheRegistry.set_cache_for(local_cache_key, nil)
79
+ end
80
+
81
+ # The current local cache.
82
+ def local_cache
83
+ LocalCacheRegistry.cache_for(local_cache_key)
84
+ end
85
+
71
86
  # Middleware class can be inserted as a Rack handler to be local cache for the
72
87
  # duration of request.
73
88
  def middleware
74
- @middleware ||= Middleware.new(
75
- "ActiveSupport::Cache::Strategy::LocalCache",
76
- local_cache_key)
89
+ @middleware ||= Middleware.new("ActiveSupport::Cache::Strategy::LocalCache", self)
77
90
  end
78
91
 
79
92
  def clear(options = nil) # :nodoc:
@@ -139,7 +152,7 @@ module ActiveSupport
139
152
  results.merge!(super(*(names - results.keys), options, &block))
140
153
  end
141
154
 
142
- results
155
+ results.slice(*names)
143
156
  end
144
157
 
145
158
  private
@@ -214,10 +227,6 @@ module ActiveSupport
214
227
  @local_cache_key ||= "#{self.class.name.underscore}_local_cache_#{object_id}".gsub(/[\/-]/, "_").to_sym
215
228
  end
216
229
 
217
- def local_cache
218
- LocalCacheRegistry.cache_for(local_cache_key)
219
- end
220
-
221
230
  def bypass_local_cache(&block)
222
231
  use_temporary_local_cache(nil, &block)
223
232
  end
@@ -11,11 +11,12 @@ module ActiveSupport
11
11
  # This class wraps up local storage for middlewares. Only the middleware method should
12
12
  # construct them.
13
13
  class Middleware # :nodoc:
14
- attr_reader :name, :local_cache_key
14
+ attr_reader :name
15
+ attr_accessor :cache
15
16
 
16
- def initialize(name, local_cache_key)
17
+ def initialize(name, cache)
17
18
  @name = name
18
- @local_cache_key = local_cache_key
19
+ @cache = cache
19
20
  @app = nil
20
21
  end
21
22
 
@@ -25,18 +26,17 @@ module ActiveSupport
25
26
  end
26
27
 
27
28
  def call(env)
28
- LocalCacheRegistry.set_cache_for(local_cache_key, LocalStore.new)
29
+ cache.new_local_cache
29
30
  response = @app.call(env)
30
31
  response[2] = ::Rack::BodyProxy.new(response[2]) do
31
- LocalCacheRegistry.set_cache_for(local_cache_key, nil)
32
+ cache.unset_local_cache
32
33
  end
33
34
  cleanup_on_body_close = true
34
35
  response
35
36
  rescue Rack::Utils::InvalidParameterError
36
37
  [400, {}, []]
37
38
  ensure
38
- LocalCacheRegistry.set_cache_for(local_cache_key, nil) unless
39
- cleanup_on_body_close
39
+ cache.unset_local_cache unless cleanup_on_body_close
40
40
  end
41
41
  end
42
42
  end