activesupport 7.2.2.2 → 7.2.3

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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +143 -0
  3. data/README.rdoc +1 -1
  4. data/lib/active_support/backtrace_cleaner.rb +1 -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 +13 -15
  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 +1 -1
  11. data/lib/active_support/cache/strategy/local_cache.rb +56 -20
  12. data/lib/active_support/cache.rb +3 -3
  13. data/lib/active_support/callbacks.rb +3 -2
  14. data/lib/active_support/core_ext/benchmark.rb +1 -0
  15. data/lib/active_support/core_ext/class/attribute.rb +2 -2
  16. data/lib/active_support/core_ext/date_time/conversions.rb +4 -2
  17. data/lib/active_support/core_ext/enumerable.rb +17 -5
  18. data/lib/active_support/core_ext/erb/util.rb +2 -2
  19. data/lib/active_support/core_ext/module/introspection.rb +3 -0
  20. data/lib/active_support/core_ext/object/try.rb +2 -2
  21. data/lib/active_support/core_ext/range/sole.rb +17 -0
  22. data/lib/active_support/core_ext/range.rb +1 -0
  23. data/lib/active_support/core_ext/securerandom.rb +24 -8
  24. data/lib/active_support/core_ext/string/filters.rb +3 -3
  25. data/lib/active_support/core_ext/string/multibyte.rb +2 -2
  26. data/lib/active_support/core_ext/time/compatibility.rb +9 -1
  27. data/lib/active_support/current_attributes.rb +14 -7
  28. data/lib/active_support/error_reporter.rb +5 -2
  29. data/lib/active_support/execution_wrapper.rb +1 -1
  30. data/lib/active_support/file_update_checker.rb +1 -1
  31. data/lib/active_support/gem_version.rb +2 -2
  32. data/lib/active_support/hash_with_indifferent_access.rb +20 -16
  33. data/lib/active_support/json/decoding.rb +1 -1
  34. data/lib/active_support/json/encoding.rb +23 -5
  35. data/lib/active_support/lazy_load_hooks.rb +1 -1
  36. data/lib/active_support/message_encryptors.rb +2 -2
  37. data/lib/active_support/message_verifier.rb +9 -0
  38. data/lib/active_support/message_verifiers.rb +5 -3
  39. data/lib/active_support/messages/rotator.rb +5 -0
  40. data/lib/active_support/multibyte/chars.rb +4 -1
  41. data/lib/active_support/testing/parallelization/server.rb +15 -2
  42. data/lib/active_support/testing/parallelization/worker.rb +2 -2
  43. data/lib/active_support/testing/parallelization.rb +12 -1
  44. data/lib/active_support/xml_mini.rb +2 -0
  45. metadata +5 -5
  46. data/lib/active_support/testing/strict_warnings.rb +0 -43
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 750ecfba98f84e525b7c36607c041c383a1a549a4066b99eff3b54cb7f6aea17
4
- data.tar.gz: 541657fdd905365f97f377d67b2cbd3f0849e6d94281bfe6da4625a09226167d
3
+ metadata.gz: d12b3bc49972cf65396ee7cb16b370f76853dde525bf3d111e6da8a5936ab441
4
+ data.tar.gz: a97522d2f10dc74dc7d3bc922eb37b3b6c369bd729e26a22e97d250602d6c544
5
5
  SHA512:
6
- metadata.gz: c3af2c496e771d723aca4f73c5d11fde1b163a975fd0652de23ef91896615af158b804e8754bf5483b2946333051c8fb0a31aacf6f13d5a314b9a8815e1cdb3c
7
- data.tar.gz: 3242b4bc37eb068eb09c191d5f4feec156dc21e565b4a9372068c22828e1c84e80fb847e04b85a7521af018ba543ba9a20e0fae25bcb9e03e83b123ec9ecd8c0
6
+ metadata.gz: 2a89e3f270073d83270087ad960f1236ee794fb833303b1e97121a1f52b20f07ca79d22c879b474d33d9180b2fbc90359619bd20886a8ab978dabcd7df3df4e2
7
+ data.tar.gz: 0576fb7d4a4bef1d8e9e8a0bbce32f0fbb74ba626e7bbcacabcf50352aaf3c2f6567e4989debe83095164b1f9a16a31ea9c0a9afe55cd4327ffc645f5acbef2d
data/CHANGELOG.md CHANGED
@@ -1,3 +1,146 @@
1
+ ## Rails 7.2.3 (October 28, 2025) ##
2
+
3
+ * Fix `Enumerable#sole` to return the full tuple instead of just the first element of the tuple.
4
+
5
+ *Olivier Bellone*
6
+
7
+ * Fix parallel tests hanging when worker processes die abruptly.
8
+
9
+ Previously, if a worker process was killed (e.g., OOM killed, `kill -9`) during parallel
10
+ test execution, the test suite would hang forever waiting for the dead worker.
11
+
12
+ *Joshua Young*
13
+
14
+ * `ActiveSupport::FileUpdateChecker` does not depend on `Time.now` to prevent unnecessary reloads with time travel test helpers
15
+
16
+ *Jan Grodowski*
17
+
18
+ * Fix `ActiveSupport::BroadcastLogger` from executing a block argument for each logger (tagged, info, etc.).
19
+
20
+ *Jared Armstrong*
21
+
22
+ * Fix `ActiveSupport::HashWithIndifferentAccess#transform_keys!` removing defaults.
23
+
24
+ *Hartley McGuire*
25
+
26
+ * Fix `ActiveSupport::HashWithIndifferentAccess#tranform_keys!` to handle collisions.
27
+
28
+ If the transformation would result in a key equal to another not yet transformed one,
29
+ it would result in keys being lost.
30
+
31
+ Before:
32
+
33
+ ```ruby
34
+ >> {a: 1, b: 2}.with_indifferent_access.transform_keys!(&:succ)
35
+ => {"c" => 1}
36
+ ```
37
+
38
+ After:
39
+
40
+ ```ruby
41
+ >> {a: 1, b: 2}.with_indifferent_access.transform_keys!(&:succ)
42
+ => {"c" => 1, "d" => 2}
43
+ ```
44
+
45
+ *Jason T Johnson*, *Jean Boussier*
46
+
47
+ * Fix `ActiveSupport::Cache::MemCacheStore#read_multi` to handle network errors.
48
+
49
+ This method specifically wasn't handling network errors like other codepaths.
50
+
51
+ *Alessandro Dal Grande*
52
+
53
+ * Fix Active Support Cache `fetch_multi` when local store is active.
54
+
55
+ `fetch_multi` now properly yield to the provided block for missing entries
56
+ that have been recorded as such in the local store.
57
+
58
+ *Jean Boussier*
59
+
60
+ * Fix execution wrapping to report all exceptions, including `Exception`.
61
+
62
+ If a more serious error like `SystemStackError` or `NoMemoryError` happens,
63
+ the error reporter should be able to report these kinds of exceptions.
64
+
65
+ *Gannon McGibbon*
66
+
67
+ * Fix `RedisCacheStore` and `MemCacheStore` to also handle connection pool related errors.
68
+
69
+ These errors are rescued and reported to `Rails.error`.
70
+
71
+ *Jean Boussier*
72
+
73
+ * Fix `ActiveSupport::Cache#read_multi` to respect version expiry when using local cache.
74
+
75
+ *zzak*
76
+
77
+ * Fix `ActiveSupport::MessageVerifier` and `ActiveSupport::MessageEncryptor` configuration of `on_rotation` callback.
78
+
79
+ ```ruby
80
+ verifier.rotate(old_secret).on_rotation { ... }
81
+ ```
82
+
83
+ Now both work as documented.
84
+
85
+ *Jean Boussier*
86
+
87
+ * Fix `ActiveSupport::MessageVerifier` to always be able to verify both URL-safe and URL-unsafe payloads.
88
+
89
+ This is to allow transitioning seemlessly from either configuration without immediately invalidating
90
+ all previously generated signed messages.
91
+
92
+ *Jean Boussier*, *Florent Beaurain*, *Ali Sepehri*
93
+
94
+ * Fix `cache.fetch` to honor the provided expiry when `:race_condition_ttl` is used.
95
+
96
+ ```ruby
97
+ cache.fetch("key", expires_in: 1.hour, race_condition_ttl: 5.second) do
98
+ "something"
99
+ end
100
+ ```
101
+
102
+ In the above example, the final cache entry would have a 10 seconds TTL instead
103
+ of the requested 1 hour.
104
+
105
+ *Dhia*
106
+
107
+ * Better handle procs with splat arguments in `set_callback`.
108
+
109
+ *Radamés Roriz*
110
+
111
+ * Fix `String#mb_chars` to not mutate the receiver.
112
+
113
+ Previously it would call `force_encoding` on the receiver,
114
+ now it dups the receiver first.
115
+
116
+ *Jean Boussier*
117
+
118
+ * Improve `ErrorSubscriber` to also mark error causes as reported.
119
+
120
+ This avoid some cases of errors being reported twice, notably in views because of how
121
+ errors are wrapped in `ActionView::Template::Error`.
122
+
123
+ *Jean Boussier*
124
+
125
+ * Fix `Module#module_parent_name` to return the correct name after the module has been named.
126
+
127
+ When called on an anonymous module, the return value wouldn't change after the module was given a name
128
+ later by being assigned to a constant.
129
+
130
+ ```ruby
131
+ mod = Module.new
132
+ mod.module_parent_name # => "Object"
133
+ MyModule::Something = mod
134
+ mod.module_parent_name # => "MyModule"
135
+ ```
136
+
137
+ *Jean Boussier*
138
+
139
+ * Fix a bug in `ERB::Util.tokenize` that causes incorrect tokenization when ERB tags are preceeded by multibyte characters.
140
+
141
+ *Martin Emde*
142
+
143
+
1
144
  ## Rails 7.2.2.2 (August 13, 2025) ##
2
145
 
3
146
  * No changes.
data/README.rdoc CHANGED
@@ -35,6 +35,6 @@ Bug reports for the Ruby on \Rails project can be filed here:
35
35
 
36
36
  * https://github.com/rails/rails/issues
37
37
 
38
- Feature requests should be discussed on the rails-core mailing list here:
38
+ Feature requests should be discussed on the rubyonrails-core forum here:
39
39
 
40
40
  * https://discuss.rubyonrails.org/c/rubyonrails-core
@@ -79,7 +79,7 @@ module ActiveSupport
79
79
  #
80
80
  # # Will turn "/my/rails/root/app/models/person.rb" into "app/models/person.rb"
81
81
  # root = "#{Rails.root}/"
82
- # backtrace_cleaner.add_filter { |line| line.start_with?(root) ? line.from(root.size) : line }
82
+ # backtrace_cleaner.add_filter { |line| line.delete_prefix(root) }
83
83
  def add_filter(&block)
84
84
  @filters << block
85
85
  end
@@ -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
  modify_value(name, amount, options)
62
62
  end
63
63
 
@@ -72,7 +72,7 @@ module ActiveSupport
72
72
  # cache.write("baz", 5)
73
73
  # cache.decrement("baz") # => 4
74
74
  #
75
- def decrement(name, amount = 1, options = nil)
75
+ def decrement(name, amount = 1, **options)
76
76
  modify_value(name, -amount, options)
77
77
  end
78
78
 
@@ -212,26 +212,24 @@ module ActiveSupport
212
212
  def read_multi_entries(names, **options)
213
213
  keys_to_names = names.index_by { |name| normalize_key(name, options) }
214
214
 
215
- raw_values = begin
216
- @data.with { |c| c.get_multi(keys_to_names.keys) }
217
- rescue Dalli::UnmarshalError
218
- {}
219
- end
215
+ rescue_error_with({}) do
216
+ raw_values = @data.with { |c| c.get_multi(keys_to_names.keys) }
220
217
 
221
- values = {}
218
+ values = {}
222
219
 
223
- raw_values.each do |key, value|
224
- entry = deserialize_entry(value, raw: options[:raw])
220
+ raw_values.each do |key, value|
221
+ entry = deserialize_entry(value, raw: options[:raw])
225
222
 
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
223
+ unless entry.nil? || entry.expired? || entry.mismatched?(normalize_version(keys_to_names[key], options))
224
+ begin
225
+ values[keys_to_names[key]] = entry.value
226
+ rescue DeserializationError
227
+ end
230
228
  end
231
229
  end
232
- end
233
230
 
234
- values
231
+ values
232
+ end
235
233
  end
236
234
 
237
235
  # Delete an entry from the cache.
@@ -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,8 +146,8 @@ module ActiveSupport
146
146
  # cache.write("baz", 5)
147
147
  # cache.increment("baz") # => 6
148
148
  #
149
- def increment(name, amount = 1, options = nil)
150
- modify_value(name, amount, options)
149
+ def increment(name, amount = 1, **options)
150
+ modify_value(name, amount, **options)
151
151
  end
152
152
 
153
153
  # Decrement a cached integer value. Returns the updated value.
@@ -161,8 +161,8 @@ module ActiveSupport
161
161
  # cache.write("baz", 5)
162
162
  # cache.decrement("baz") # => 4
163
163
  #
164
- def decrement(name, amount = 1, options = nil)
165
- modify_value(name, -amount, options)
164
+ def decrement(name, amount = 1, **options)
165
+ modify_value(name, -amount, **options)
166
166
  end
167
167
 
168
168
  # Deletes cache entries if the cache key matches a given pattern.
@@ -234,7 +234,7 @@ module ActiveSupport
234
234
 
235
235
  # Modifies the amount of an integer value that is stored in the cache.
236
236
  # If the key is not found it is created and set to +amount+.
237
- def modify_value(name, amount, options)
237
+ def modify_value(name, amount, **options)
238
238
  options = merged_options(options)
239
239
  key = normalize_key(name, options)
240
240
  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)
@@ -480,7 +480,7 @@ module ActiveSupport
480
480
 
481
481
  def failsafe(method, returning: nil)
482
482
  yield
483
- rescue ::Redis::BaseError => error
483
+ rescue ::Redis::BaseError, ConnectionPool::Error, ConnectionPool::TimeoutError => error
484
484
  @error_handler&.call(method: method, exception: error, returning: returning)
485
485
  returning
486
486
  end
@@ -94,28 +94,54 @@ module ActiveSupport
94
94
  super
95
95
  end
96
96
 
97
- def increment(name, amount = 1, options = nil) # :nodoc:
97
+ def increment(name, amount = 1, **options) # :nodoc:
98
98
  return super unless local_cache
99
99
  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
100
+ write_cache_value(name, value, raw: true, **options)
105
101
  value
106
102
  end
107
103
 
108
- def decrement(name, amount = 1, options = nil) # :nodoc:
104
+ def decrement(name, amount = 1, **options) # :nodoc:
109
105
  return super unless local_cache
110
106
  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
107
+ write_cache_value(name, value, raw: true, **options)
116
108
  value
117
109
  end
118
110
 
111
+ def fetch_multi(*names, &block) # :nodoc:
112
+ return super if local_cache.nil? || names.empty?
113
+
114
+ options = names.extract_options!
115
+ options = merged_options(options)
116
+
117
+ keys_to_names = names.index_by { |name| normalize_key(name, options) }
118
+
119
+ local_entries = local_cache.read_multi_entries(keys_to_names.keys)
120
+ results = local_entries.each_with_object({}) do |(key, value), result|
121
+ # If we recorded a miss in the local cache, `#fetch_multi` will forward
122
+ # that key to the real store, and the entry will be replaced
123
+ # local_cache.delete_entry(key)
124
+ next if value.nil?
125
+
126
+ entry = deserialize_entry(value, **options)
127
+
128
+ normalized_key = keys_to_names[key]
129
+ if entry.nil?
130
+ result[normalized_key] = nil
131
+ elsif entry.expired? || entry.mismatched?(normalize_version(normalized_key, options))
132
+ local_cache.delete_entry(key)
133
+ else
134
+ result[normalized_key] = entry.value
135
+ end
136
+ end
137
+
138
+ if results.size < names.size
139
+ results.merge!(super(*(names - results.keys), options, &block))
140
+ end
141
+
142
+ results
143
+ end
144
+
119
145
  private
120
146
  def read_serialized_entry(key, raw: false, **options)
121
147
  if cache = local_cache
@@ -137,17 +163,27 @@ module ActiveSupport
137
163
  keys_to_names = names.index_by { |name| normalize_key(name, options) }
138
164
 
139
165
  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
166
+
167
+ results = local_entries.each_with_object({}) do |(key, value), result|
168
+ next if value.nil? # recorded cache miss
169
+
170
+ entry = deserialize_entry(value, **options)
171
+
172
+ normalized_key = keys_to_names[key]
173
+ if entry.nil?
174
+ result[normalized_key] = nil
175
+ elsif entry.expired? || entry.mismatched?(normalize_version(normalized_key, options))
176
+ local_cache.delete_entry(key)
177
+ else
178
+ result[normalized_key] = entry.value
179
+ end
143
180
  end
144
- missed_names = names - local_entries.keys
145
181
 
146
- if missed_names.any?
147
- local_entries.merge!(super(missed_names, **options))
148
- else
149
- local_entries
182
+ if results.size < names.size
183
+ results.merge!(super(names - results.keys, **options))
150
184
  end
185
+
186
+ results
151
187
  end
152
188
 
153
189
  def write_serialized_entry(key, payload, **)
@@ -35,6 +35,7 @@ module ActiveSupport
35
35
  :race_condition_ttl,
36
36
  :serializer,
37
37
  :skip_nil,
38
+ :raw,
38
39
  ]
39
40
 
40
41
  # Mapping of canonical option names to aliases that a store will recognize.
@@ -386,7 +387,7 @@ module ActiveSupport
386
387
  # process can try to generate a new value after the extended time window
387
388
  # has elapsed.
388
389
  #
389
- # # Set all values to expire after one minute.
390
+ # # Set all values to expire after one second.
390
391
  # cache = ActiveSupport::Cache::MemoryStore.new(expires_in: 1)
391
392
  #
392
393
  # cache.write("foo", "original value")
@@ -1030,8 +1031,7 @@ module ActiveSupport
1030
1031
  # When an entry has a positive :race_condition_ttl defined, put the stale entry back into the cache
1031
1032
  # for a brief period while the entry is being recalculated.
1032
1033
  entry.expires_at = Time.now.to_f + race_ttl
1033
- options[:expires_in] = race_ttl * 2
1034
- write_entry(key, entry, **options)
1034
+ write_entry(key, entry, **options, expires_in: race_ttl * 2)
1035
1035
  else
1036
1036
  delete_entry(key, **options)
1037
1037
  end
@@ -499,9 +499,10 @@ module ActiveSupport
499
499
  when Conditionals::Value
500
500
  ProcCall.new(filter)
501
501
  when ::Proc
502
- if filter.arity > 1
502
+ case filter.arity
503
+ when 2
503
504
  InstanceExec2.new(filter)
504
- elsif filter.arity > 0
505
+ when 1, -2
505
506
  InstanceExec1.new(filter)
506
507
  else
507
508
  InstanceExec0.new(filter)
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "benchmark"
4
+ return if Benchmark.respond_to?(:ms)
4
5
 
5
6
  class << Benchmark
6
7
  # Benchmark realtime in milliseconds.
@@ -83,8 +83,8 @@ class Class
83
83
  #
84
84
  # class_attribute :settings, default: {}
85
85
  def class_attribute(*attrs, instance_accessor: true,
86
- instance_reader: instance_accessor, instance_writer: instance_accessor, instance_predicate: true, default: nil)
87
-
86
+ instance_reader: instance_accessor, instance_writer: instance_accessor, instance_predicate: true, default: nil
87
+ )
88
88
  class_methods, methods = [], []
89
89
  attrs.each do |name|
90
90
  unless name.is_a?(Symbol) || name.is_a?(String)