activesupport 8.0.0 → 8.1.0

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 (116) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +391 -67
  3. data/README.rdoc +1 -1
  4. data/lib/active_support/backtrace_cleaner.rb +72 -1
  5. data/lib/active_support/broadcast_logger.rb +61 -74
  6. data/lib/active_support/cache/file_store.rb +2 -2
  7. data/lib/active_support/cache/mem_cache_store.rb +26 -28
  8. data/lib/active_support/cache/memory_store.rb +5 -5
  9. data/lib/active_support/cache/null_store.rb +2 -2
  10. data/lib/active_support/cache/redis_cache_store.rb +37 -31
  11. data/lib/active_support/cache/strategy/local_cache.rb +72 -27
  12. data/lib/active_support/cache/strategy/local_cache_middleware.rb +7 -7
  13. data/lib/active_support/cache.rb +72 -9
  14. data/lib/active_support/callbacks.rb +27 -11
  15. data/lib/active_support/class_attribute.rb +15 -8
  16. data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +8 -62
  17. data/lib/active_support/concurrency/thread_monitor.rb +55 -0
  18. data/lib/active_support/configurable.rb +34 -0
  19. data/lib/active_support/continuous_integration.rb +145 -0
  20. data/lib/active_support/core_ext/array/conversions.rb +3 -3
  21. data/lib/active_support/core_ext/array.rb +7 -7
  22. data/lib/active_support/core_ext/benchmark.rb +4 -11
  23. data/lib/active_support/core_ext/big_decimal.rb +1 -1
  24. data/lib/active_support/core_ext/class/attribute.rb +20 -5
  25. data/lib/active_support/core_ext/class.rb +2 -2
  26. data/lib/active_support/core_ext/date.rb +5 -5
  27. data/lib/active_support/core_ext/date_and_time/compatibility.rb +0 -35
  28. data/lib/active_support/core_ext/date_time/compatibility.rb +3 -5
  29. data/lib/active_support/core_ext/date_time/conversions.rb +4 -2
  30. data/lib/active_support/core_ext/date_time.rb +5 -5
  31. data/lib/active_support/core_ext/digest.rb +1 -1
  32. data/lib/active_support/core_ext/enumerable.rb +17 -5
  33. data/lib/active_support/core_ext/erb/util.rb +5 -5
  34. data/lib/active_support/core_ext/file.rb +1 -1
  35. data/lib/active_support/core_ext/hash/deep_merge.rb +1 -0
  36. data/lib/active_support/core_ext/hash.rb +8 -8
  37. data/lib/active_support/core_ext/integer.rb +3 -3
  38. data/lib/active_support/core_ext/kernel.rb +3 -3
  39. data/lib/active_support/core_ext/module/introspection.rb +3 -0
  40. data/lib/active_support/core_ext/module.rb +11 -11
  41. data/lib/active_support/core_ext/numeric.rb +3 -3
  42. data/lib/active_support/core_ext/object/json.rb +8 -1
  43. data/lib/active_support/core_ext/object/to_query.rb +7 -1
  44. data/lib/active_support/core_ext/object/try.rb +2 -2
  45. data/lib/active_support/core_ext/object.rb +13 -13
  46. data/lib/active_support/core_ext/pathname.rb +2 -2
  47. data/lib/active_support/core_ext/range/overlap.rb +3 -3
  48. data/lib/active_support/core_ext/range/sole.rb +17 -0
  49. data/lib/active_support/core_ext/range.rb +4 -4
  50. data/lib/active_support/core_ext/securerandom.rb +24 -8
  51. data/lib/active_support/core_ext/string/filters.rb +3 -3
  52. data/lib/active_support/core_ext/string/multibyte.rb +12 -3
  53. data/lib/active_support/core_ext/string/output_safety.rb +19 -12
  54. data/lib/active_support/core_ext/string.rb +13 -13
  55. data/lib/active_support/core_ext/symbol.rb +1 -1
  56. data/lib/active_support/core_ext/time/calculations.rb +0 -7
  57. data/lib/active_support/core_ext/time/compatibility.rb +2 -27
  58. data/lib/active_support/core_ext/time.rb +5 -5
  59. data/lib/active_support/core_ext.rb +1 -1
  60. data/lib/active_support/current_attributes/test_helper.rb +2 -2
  61. data/lib/active_support/current_attributes.rb +27 -17
  62. data/lib/active_support/dependencies/interlock.rb +11 -5
  63. data/lib/active_support/dependencies.rb +6 -1
  64. data/lib/active_support/deprecation/reporting.rb +4 -2
  65. data/lib/active_support/deprecation.rb +1 -1
  66. data/lib/active_support/editor.rb +70 -0
  67. data/lib/active_support/error_reporter.rb +61 -8
  68. data/lib/active_support/event_reporter/test_helper.rb +32 -0
  69. data/lib/active_support/event_reporter.rb +592 -0
  70. data/lib/active_support/evented_file_update_checker.rb +5 -1
  71. data/lib/active_support/execution_context.rb +64 -7
  72. data/lib/active_support/execution_wrapper.rb +1 -1
  73. data/lib/active_support/file_update_checker.rb +8 -6
  74. data/lib/active_support/gem_version.rb +1 -1
  75. data/lib/active_support/gzip.rb +1 -0
  76. data/lib/active_support/hash_with_indifferent_access.rb +47 -24
  77. data/lib/active_support/i18n_railtie.rb +1 -2
  78. data/lib/active_support/inflector/inflections.rb +31 -15
  79. data/lib/active_support/inflector/transliterate.rb +6 -8
  80. data/lib/active_support/isolated_execution_state.rb +12 -15
  81. data/lib/active_support/json/decoding.rb +6 -4
  82. data/lib/active_support/json/encoding.rb +155 -19
  83. data/lib/active_support/lazy_load_hooks.rb +1 -1
  84. data/lib/active_support/log_subscriber.rb +2 -6
  85. data/lib/active_support/logger_thread_safe_level.rb +6 -3
  86. data/lib/active_support/message_encryptors.rb +54 -2
  87. data/lib/active_support/message_pack/extensions.rb +5 -0
  88. data/lib/active_support/message_verifier.rb +9 -0
  89. data/lib/active_support/message_verifiers.rb +57 -3
  90. data/lib/active_support/messages/rotation_coordinator.rb +9 -0
  91. data/lib/active_support/messages/rotator.rb +10 -0
  92. data/lib/active_support/multibyte/chars.rb +12 -2
  93. data/lib/active_support/multibyte.rb +4 -0
  94. data/lib/active_support/notifications/fanout.rb +64 -42
  95. data/lib/active_support/notifications/instrumenter.rb +1 -1
  96. data/lib/active_support/railtie.rb +32 -13
  97. data/lib/active_support/structured_event_subscriber.rb +99 -0
  98. data/lib/active_support/subscriber.rb +0 -5
  99. data/lib/active_support/syntax_error_proxy.rb +3 -0
  100. data/lib/active_support/test_case.rb +61 -6
  101. data/lib/active_support/testing/assertions.rb +34 -6
  102. data/lib/active_support/testing/error_reporter_assertions.rb +18 -1
  103. data/lib/active_support/testing/event_reporter_assertions.rb +227 -0
  104. data/lib/active_support/testing/notification_assertions.rb +92 -0
  105. data/lib/active_support/testing/parallelization/server.rb +15 -2
  106. data/lib/active_support/testing/parallelization/worker.rb +4 -2
  107. data/lib/active_support/testing/parallelization.rb +25 -1
  108. data/lib/active_support/testing/tests_without_assertions.rb +1 -1
  109. data/lib/active_support/testing/time_helpers.rb +7 -3
  110. data/lib/active_support/time_with_zone.rb +23 -23
  111. data/lib/active_support/values/time_zone.rb +8 -1
  112. data/lib/active_support/xml_mini.rb +3 -2
  113. data/lib/active_support.rb +20 -15
  114. metadata +27 -23
  115. data/lib/active_support/core_ext/range/each.rb +0 -24
  116. data/lib/active_support/testing/strict_warnings.rb +0 -43
@@ -76,7 +76,6 @@ module ActiveSupport
76
76
 
77
77
  # Returns all the logger that are part of this broadcast.
78
78
  attr_reader :broadcasts
79
- attr_reader :formatter
80
79
  attr_accessor :progname
81
80
 
82
81
  def initialize(*loggers)
@@ -105,131 +104,119 @@ module ActiveSupport
105
104
  @broadcasts.delete(logger)
106
105
  end
107
106
 
108
- def level
109
- @broadcasts.map(&:level).min
110
- end
111
-
112
- def <<(message)
113
- dispatch { |logger| logger.<<(message) }
114
- end
115
-
116
- def add(...)
117
- dispatch { |logger| logger.add(...) }
118
- end
119
- alias_method :log, :add
120
-
121
- def debug(...)
122
- dispatch { |logger| logger.debug(...) }
123
- end
124
-
125
- def info(...)
126
- dispatch { |logger| logger.info(...) }
127
- end
128
-
129
- def warn(...)
130
- dispatch { |logger| logger.warn(...) }
131
- end
132
-
133
- def error(...)
134
- dispatch { |logger| logger.error(...) }
135
- end
136
-
137
- def fatal(...)
138
- dispatch { |logger| logger.fatal(...) }
139
- end
140
-
141
- def unknown(...)
142
- dispatch { |logger| logger.unknown(...) }
107
+ def local_level=(level)
108
+ @broadcasts.each do |logger|
109
+ logger.local_level = level if logger.respond_to?(:local_level=)
110
+ end
143
111
  end
144
112
 
145
- def formatter=(formatter)
146
- dispatch { |logger| logger.formatter = formatter }
147
-
148
- @formatter = formatter
149
- end
113
+ def local_level
114
+ loggers = @broadcasts.select { |logger| logger.respond_to?(:local_level) }
150
115
 
151
- def level=(level)
152
- dispatch { |logger| logger.level = level }
116
+ loggers.map do |logger|
117
+ logger.local_level
118
+ end.first
153
119
  end
154
- alias_method :sev_threshold=, :level=
155
120
 
156
- def local_level=(level)
157
- dispatch do |logger|
158
- logger.local_level = level if logger.respond_to?(:local_level=)
159
- end
121
+ LOGGER_METHODS = %w[
122
+ << log add debug info warn error fatal unknown
123
+ level= sev_threshold= close
124
+ formatter formatter=
125
+ ] # :nodoc:
126
+ LOGGER_METHODS.each do |method|
127
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
128
+ def #{method}(...)
129
+ dispatch(:#{method}, ...)
130
+ end
131
+ RUBY
160
132
  end
161
133
 
162
- def close
163
- dispatch { |logger| logger.close }
134
+ # Returns the lowest level of all the loggers in the broadcast.
135
+ def level
136
+ @broadcasts.map(&:level).min
164
137
  end
165
138
 
166
- # +True+ if the log level allows entries with severity Logger::DEBUG to be written
167
- # to at least one broadcast. +False+ otherwise.
139
+ # True if the log level allows entries with severity +Logger::DEBUG+ to be written
140
+ # to at least one broadcast. False otherwise.
168
141
  def debug?
169
142
  @broadcasts.any? { |logger| logger.debug? }
170
143
  end
171
144
 
172
- # Sets the log level to Logger::DEBUG for the whole broadcast.
145
+ # Sets the log level to +Logger::DEBUG+ for the whole broadcast.
173
146
  def debug!
174
- dispatch { |logger| logger.debug! }
147
+ dispatch(:debug!)
175
148
  end
176
149
 
177
- # +True+ if the log level allows entries with severity Logger::INFO to be written
178
- # to at least one broadcast. +False+ otherwise.
150
+ # True if the log level allows entries with severity +Logger::INFO+ to be written
151
+ # to at least one broadcast. False otherwise.
179
152
  def info?
180
153
  @broadcasts.any? { |logger| logger.info? }
181
154
  end
182
155
 
183
- # Sets the log level to Logger::INFO for the whole broadcast.
156
+ # Sets the log level to +Logger::INFO+ for the whole broadcast.
184
157
  def info!
185
- dispatch { |logger| logger.info! }
158
+ dispatch(:info!)
186
159
  end
187
160
 
188
- # +True+ if the log level allows entries with severity Logger::WARN to be written
189
- # to at least one broadcast. +False+ otherwise.
161
+ # True if the log level allows entries with severity +Logger::WARN+ to be written
162
+ # to at least one broadcast. False otherwise.
190
163
  def warn?
191
164
  @broadcasts.any? { |logger| logger.warn? }
192
165
  end
193
166
 
194
- # Sets the log level to Logger::WARN for the whole broadcast.
167
+ # Sets the log level to +Logger::WARN+ for the whole broadcast.
195
168
  def warn!
196
- dispatch { |logger| logger.warn! }
169
+ dispatch(:warn!)
197
170
  end
198
171
 
199
- # +True+ if the log level allows entries with severity Logger::ERROR to be written
200
- # to at least one broadcast. +False+ otherwise.
172
+ # True if the log level allows entries with severity +Logger::ERROR+ to be written
173
+ # to at least one broadcast. False otherwise.
201
174
  def error?
202
175
  @broadcasts.any? { |logger| logger.error? }
203
176
  end
204
177
 
205
- # Sets the log level to Logger::ERROR for the whole broadcast.
178
+ # Sets the log level to +Logger::ERROR+ for the whole broadcast.
206
179
  def error!
207
- dispatch { |logger| logger.error! }
180
+ dispatch(:error!)
208
181
  end
209
182
 
210
- # +True+ if the log level allows entries with severity Logger::FATAL to be written
211
- # to at least one broadcast. +False+ otherwise.
183
+ # True if the log level allows entries with severity +Logger::FATAL+ to be written
184
+ # to at least one broadcast. False otherwise.
212
185
  def fatal?
213
186
  @broadcasts.any? { |logger| logger.fatal? }
214
187
  end
215
188
 
216
- # Sets the log level to Logger::FATAL for the whole broadcast.
189
+ # Sets the log level to +Logger::FATAL+ for the whole broadcast.
217
190
  def fatal!
218
- dispatch { |logger| logger.fatal! }
191
+ dispatch(:fatal!)
219
192
  end
220
193
 
221
194
  def initialize_copy(other)
222
195
  @broadcasts = []
223
196
  @progname = other.progname.dup
224
- @formatter = other.formatter.dup
225
197
 
226
198
  broadcast_to(*other.broadcasts.map(&:dup))
227
199
  end
228
200
 
229
201
  private
230
- def dispatch(&block)
231
- @broadcasts.each { |logger| block.call(logger) }
232
- true
202
+ def dispatch(method, *args, **kwargs, &block)
203
+ if block_given?
204
+ # Maintain semantics that the first logger yields the block
205
+ # as normal, but subsequent loggers won't re-execute the block.
206
+ # Instead, the initial result is immediately returned.
207
+ called, result = false, nil
208
+ block = proc { |*args, **kwargs|
209
+ if called then result
210
+ else
211
+ called = true
212
+ result = yield(*args, **kwargs)
213
+ end
214
+ }
215
+ end
216
+
217
+ @broadcasts.map { |logger|
218
+ logger.send(method, *args, **kwargs, &block)
219
+ }.first
233
220
  end
234
221
 
235
222
  def method_missing(name, ...)
@@ -57,7 +57,7 @@ module ActiveSupport
57
57
  # cache.write("baz", 5)
58
58
  # cache.increment("baz") # => 6
59
59
  #
60
- def increment(name, amount = 1, options = nil)
60
+ def increment(name, amount = 1, **options)
61
61
  options = merged_options(options)
62
62
  key = normalize_key(name, options)
63
63
 
@@ -77,7 +77,7 @@ module ActiveSupport
77
77
  # cache.write("baz", 5)
78
78
  # cache.decrement("baz") # => 4
79
79
  #
80
- def decrement(name, amount = 1, options = nil)
80
+ def decrement(name, amount = 1, **options)
81
81
  options = merged_options(options)
82
82
  key = normalize_key(name, options)
83
83
 
@@ -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.
@@ -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)
@@ -110,9 +110,6 @@ module ActiveSupport
110
110
  # * <tt>raw: true</tt> - Sends the value directly to the server as raw
111
111
  # bytes. The value must be a string or number. You can use memcached
112
112
  # 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
113
 
117
114
  # Increment a cached integer value using the memcached incr atomic operator.
118
115
  # Returns the updated value.
@@ -129,6 +126,11 @@ module ActiveSupport
129
126
  #
130
127
  # Incrementing a non-numeric value, or a value written without
131
128
  # <tt>raw: true</tt>, will fail and return +nil+.
129
+ #
130
+ # To read the value later, call #read_counter:
131
+ #
132
+ # cache.increment("baz") # => 7
133
+ # cache.read_counter("baz") # 7
132
134
  def increment(name, amount = 1, options = nil)
133
135
  options = merged_options(options)
134
136
  key = normalize_key(name, options)
@@ -155,6 +157,11 @@ module ActiveSupport
155
157
  #
156
158
  # Decrementing a non-numeric value, or a value written without
157
159
  # <tt>raw: true</tt>, will fail and return +nil+.
160
+ #
161
+ # To read the value later, call #read_counter:
162
+ #
163
+ # cache.decrement("baz") # => 3
164
+ # cache.read_counter("baz") # 3
158
165
  def decrement(name, amount = 1, options = nil)
159
166
  options = merged_options(options)
160
167
  key = normalize_key(name, options)
@@ -212,26 +219,24 @@ module ActiveSupport
212
219
  def read_multi_entries(names, **options)
213
220
  keys_to_names = names.index_by { |name| normalize_key(name, options) }
214
221
 
215
- raw_values = begin
216
- @data.with { |c| c.get_multi(keys_to_names.keys) }
217
- rescue Dalli::UnmarshalError
218
- {}
219
- end
222
+ rescue_error_with({}) do
223
+ raw_values = @data.with { |c| c.get_multi(keys_to_names.keys) }
220
224
 
221
- values = {}
225
+ values = {}
222
226
 
223
- raw_values.each do |key, value|
224
- entry = deserialize_entry(value, raw: options[:raw])
227
+ raw_values.each do |key, value|
228
+ entry = deserialize_entry(value, raw: options[:raw])
225
229
 
226
- unless entry.nil? || entry.expired? || entry.mismatched?(normalize_version(keys_to_names[key], options))
227
- begin
228
- values[keys_to_names[key]] = entry.value
229
- rescue DeserializationError
230
+ unless entry.nil? || entry.expired? || entry.mismatched?(normalize_version(keys_to_names[key], options))
231
+ begin
232
+ values[keys_to_names[key]] = entry.value
233
+ rescue DeserializationError
234
+ end
230
235
  end
231
236
  end
232
- end
233
237
 
234
- values
238
+ values
239
+ end
235
240
  end
236
241
 
237
242
  # Delete an entry from the cache.
@@ -251,19 +256,12 @@ module ActiveSupport
251
256
  # before applying the regular expression to ensure we are escaping all
252
257
  # characters properly.
253
258
  def normalize_key(key, options)
254
- key = super
259
+ key = expand_and_namespace_key(key, options)
255
260
  if key
256
261
  key = key.dup.force_encoding(Encoding::ASCII_8BIT)
257
262
  key = key.gsub(ESCAPE_KEY_CHARS) { |match| "%#{match.getbyte(0).to_s(16).upcase}" }
258
-
259
- if key.size > KEY_MAX_SIZE
260
- key_separator = ":hash:"
261
- key_hash = ActiveSupport::Digest.hexdigest(key)
262
- key_trim_size = KEY_MAX_SIZE - key_separator.size - key_hash.size
263
- key = "#{key[0, key_trim_size]}#{key_separator}#{key_hash}"
264
- end
265
263
  end
266
- key
264
+ truncate_key(key)
267
265
  end
268
266
 
269
267
  def deserialize_entry(payload, raw: false, **)
@@ -276,7 +274,7 @@ module ActiveSupport
276
274
 
277
275
  def rescue_error_with(fallback)
278
276
  yield
279
- rescue Dalli::DalliError => error
277
+ rescue Dalli::DalliError, ConnectionPool::Error, ConnectionPool::TimeoutError => error
280
278
  logger.error("DalliError (#{error}): #{error.message}") if logger
281
279
  ActiveSupport.error_reporter&.report(
282
280
  error,
@@ -146,9 +146,9 @@ module ActiveSupport
146
146
  # cache.write("baz", 5)
147
147
  # cache.increment("baz") # => 6
148
148
  #
149
- def increment(name, amount = 1, options = nil)
149
+ def increment(name, amount = 1, **options)
150
150
  instrument(:increment, name, amount: amount) do
151
- modify_value(name, amount, options)
151
+ modify_value(name, amount, **options)
152
152
  end
153
153
  end
154
154
 
@@ -163,9 +163,9 @@ module ActiveSupport
163
163
  # cache.write("baz", 5)
164
164
  # cache.decrement("baz") # => 4
165
165
  #
166
- def decrement(name, amount = 1, options = nil)
166
+ def decrement(name, amount = 1, **options)
167
167
  instrument(:decrement, name, amount: amount) do
168
- modify_value(name, -amount, options)
168
+ modify_value(name, -amount, **options)
169
169
  end
170
170
  end
171
171
 
@@ -238,7 +238,7 @@ module ActiveSupport
238
238
 
239
239
  # Modifies the amount of an integer value that is stored in the cache.
240
240
  # If the key is not found it is created and set to +amount+.
241
- def modify_value(name, amount, options)
241
+ def modify_value(name, amount, **options)
242
242
  options = merged_options(options)
243
243
  key = normalize_key(name, options)
244
244
  version = normalize_version(name, options)
@@ -25,10 +25,10 @@ module ActiveSupport
25
25
  def cleanup(options = nil)
26
26
  end
27
27
 
28
- def increment(name, amount = 1, options = nil)
28
+ def increment(name, amount = 1, **options)
29
29
  end
30
30
 
31
- def decrement(name, amount = 1, options = nil)
31
+ def decrement(name, amount = 1, **options)
32
32
  end
33
33
 
34
34
  def delete_matched(matcher, options = nil)
@@ -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]
158
+
159
+ already_pool = redis.instance_of?(::ConnectionPool) ||
160
+ (redis.respond_to?(:wrapped_pool) && redis.wrapped_pool.instance_of?(::ConnectionPool))
151
161
 
152
- if pool_options = self.class.send(:retrieve_pool_options, redis_options)
162
+ if !already_pool && pool_options = self.class.send(:retrieve_pool_options, redis_options)
153
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)
@@ -213,7 +222,7 @@ module ActiveSupport
213
222
  nodes.each do |node|
214
223
  begin
215
224
  cursor, keys = node.scan(cursor, match: pattern, count: SCAN_BATCH_SIZE)
216
- node.del(*keys) unless keys.empty?
225
+ node.unlink(*keys) unless keys.empty?
217
226
  end until cursor == "0"
218
227
  end
219
228
  end
@@ -236,6 +245,11 @@ module ActiveSupport
236
245
  # Incrementing a non-numeric value, or a value written without
237
246
  # <tt>raw: true</tt>, will fail and return +nil+.
238
247
  #
248
+ # To read the value later, call #read_counter:
249
+ #
250
+ # cache.increment("baz") # => 7
251
+ # cache.read_counter("baz") # 7
252
+ #
239
253
  # Failsafe: Raises errors.
240
254
  def increment(name, amount = 1, options = nil)
241
255
  options = merged_options(options)
@@ -263,6 +277,11 @@ module ActiveSupport
263
277
  # Decrementing a non-numeric value, or a value written without
264
278
  # <tt>raw: true</tt>, will fail and return +nil+.
265
279
  #
280
+ # To read the value later, call #read_counter:
281
+ #
282
+ # cache.decrement("baz") # => 3
283
+ # cache.read_counter("baz") # 3
284
+ #
266
285
  # Failsafe: Raises errors.
267
286
  def decrement(name, amount = 1, options = nil)
268
287
  options = merged_options(options)
@@ -384,14 +403,16 @@ module ActiveSupport
384
403
  # Delete an entry from the cache.
385
404
  def delete_entry(key, **options)
386
405
  failsafe :delete_entry, returning: false do
387
- redis.then { |c| c.del(key) == 1 }
406
+ redis.then { |c| c.unlink(key) == 1 }
388
407
  end
389
408
  end
390
409
 
391
410
  # Deletes multiple entries in the cache. Returns the number of entries deleted.
392
411
  def delete_multi_entries(entries, **_options)
412
+ return 0 if entries.empty?
413
+
393
414
  failsafe :delete_multi_entries, returning: 0 do
394
- redis.then { |c| c.del(entries) }
415
+ redis.then { |c| c.unlink(*entries) }
395
416
  end
396
417
  end
397
418
 
@@ -410,21 +431,6 @@ module ActiveSupport
410
431
  end
411
432
  end
412
433
 
413
- # Truncate keys that exceed 1kB.
414
- def normalize_key(key, options)
415
- truncate_key super&.b
416
- end
417
-
418
- def truncate_key(key)
419
- if key && key.bytesize > max_key_bytesize
420
- suffix = ":hash:#{ActiveSupport::Digest.hexdigest(key)}"
421
- truncate_at = max_key_bytesize - suffix.bytesize
422
- "#{key.byteslice(0, truncate_at)}#{suffix}"
423
- else
424
- key
425
- end
426
- end
427
-
428
434
  def deserialize_entry(payload, raw: false, **)
429
435
  if raw && !payload.nil?
430
436
  Entry.new(payload)
@@ -483,7 +489,7 @@ module ActiveSupport
483
489
 
484
490
  def failsafe(method, returning: nil)
485
491
  yield
486
- rescue ::Redis::BaseError => error
492
+ rescue ::Redis::BaseError, ConnectionPool::Error, ConnectionPool::TimeoutError => error
487
493
  @error_handler&.call(method: method, exception: error, returning: returning)
488
494
  returning
489
495
  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:
@@ -94,28 +107,54 @@ module ActiveSupport
94
107
  super
95
108
  end
96
109
 
97
- def increment(name, amount = 1, options = nil) # :nodoc:
110
+ def increment(name, amount = 1, **options) # :nodoc:
98
111
  return super unless local_cache
99
112
  value = bypass_local_cache { super }
100
- if options
101
- write_cache_value(name, value, raw: true, **options)
102
- else
103
- write_cache_value(name, value, raw: true)
104
- end
113
+ write_cache_value(name, value, raw: true, **options)
105
114
  value
106
115
  end
107
116
 
108
- def decrement(name, amount = 1, options = nil) # :nodoc:
117
+ def decrement(name, amount = 1, **options) # :nodoc:
109
118
  return super unless local_cache
110
119
  value = bypass_local_cache { super }
111
- if options
112
- write_cache_value(name, value, raw: true, **options)
113
- else
114
- write_cache_value(name, value, raw: true)
115
- end
120
+ write_cache_value(name, value, raw: true, **options)
116
121
  value
117
122
  end
118
123
 
124
+ def fetch_multi(*names, &block) # :nodoc:
125
+ return super if local_cache.nil? || names.empty?
126
+
127
+ options = names.extract_options!
128
+ options = merged_options(options)
129
+
130
+ keys_to_names = names.index_by { |name| normalize_key(name, options) }
131
+
132
+ local_entries = local_cache.read_multi_entries(keys_to_names.keys)
133
+ results = local_entries.each_with_object({}) do |(key, value), result|
134
+ # If we recorded a miss in the local cache, `#fetch_multi` will forward
135
+ # that key to the real store, and the entry will be replaced
136
+ # local_cache.delete_entry(key)
137
+ next if value.nil?
138
+
139
+ entry = deserialize_entry(value, **options)
140
+
141
+ normalized_key = keys_to_names[key]
142
+ if entry.nil?
143
+ result[normalized_key] = nil
144
+ elsif entry.expired? || entry.mismatched?(normalize_version(normalized_key, options))
145
+ local_cache.delete_entry(key)
146
+ else
147
+ result[normalized_key] = entry.value
148
+ end
149
+ end
150
+
151
+ if results.size < names.size
152
+ results.merge!(super(*(names - results.keys), options, &block))
153
+ end
154
+
155
+ results
156
+ end
157
+
119
158
  private
120
159
  def read_serialized_entry(key, raw: false, **options)
121
160
  if cache = local_cache
@@ -137,17 +176,27 @@ module ActiveSupport
137
176
  keys_to_names = names.index_by { |name| normalize_key(name, options) }
138
177
 
139
178
  local_entries = local_cache.read_multi_entries(keys_to_names.keys)
140
- local_entries.transform_keys! { |key| keys_to_names[key] }
141
- local_entries.transform_values! do |payload|
142
- deserialize_entry(payload, **options)&.value
179
+
180
+ results = local_entries.each_with_object({}) do |(key, value), result|
181
+ next if value.nil? # recorded cache miss
182
+
183
+ entry = deserialize_entry(value, **options)
184
+
185
+ normalized_key = keys_to_names[key]
186
+ if entry.nil?
187
+ result[normalized_key] = nil
188
+ elsif entry.expired? || entry.mismatched?(normalize_version(normalized_key, options))
189
+ local_cache.delete_entry(key)
190
+ else
191
+ result[normalized_key] = entry.value
192
+ end
143
193
  end
144
- missed_names = names - local_entries.keys
145
194
 
146
- if missed_names.any?
147
- local_entries.merge!(super(missed_names, **options))
148
- else
149
- local_entries
195
+ if results.size < names.size
196
+ results.merge!(super(names - results.keys, **options))
150
197
  end
198
+
199
+ results
151
200
  end
152
201
 
153
202
  def write_serialized_entry(key, payload, **)
@@ -178,10 +227,6 @@ module ActiveSupport
178
227
  @local_cache_key ||= "#{self.class.name.underscore}_local_cache_#{object_id}".gsub(/[\/-]/, "_").to_sym
179
228
  end
180
229
 
181
- def local_cache
182
- LocalCacheRegistry.cache_for(local_cache_key)
183
- end
184
-
185
230
  def bypass_local_cache(&block)
186
231
  use_temporary_local_cache(nil, &block)
187
232
  end