Although disallowed warnings are likely to be deprecator-specific, it
can be useful to set this value once and have it applied to any
deprecators that are added later. For example, setting the value in an
initializer via `config.active_support.disallowed_deprecation_warnings`
and applying it to any third-party deprecators that are added later in
the boot process. Also, when using `:all`, it is more convenient to
write `deprecators.disallowed_warnings = :all` instead of
`deprecators.each { |deprecator| deprecator.disallowed_warnings = :all }`.
With Ruby 3.2 or the upcoming release of erb.gem, ERB::Util#html_escape
will be faster than CGI.escapeHTML in the case that it escapes nothing
because ERB::Util#html_escape will not duplicate the argument, unlike
CGI.escapeHTML. https://bugs.ruby-lang.org/issues/19102
Calling the original ERB::Util#html_escape with `super` is preferable
not only for the above performance reason but also for making it more
permissive about other monkey patches using Module#prepend. I tried to
use Module#prepend there myself, but I gave it up because it wasn't
compatible with the monkey patch of Rails not using Module#prepend.
Ref: https://github.com/rails/rails/pull/28083
Right now all database operations are synchronized with a monitor even though
it's useless in the vast majority of cases.
The synchronization is only useful when transactional fixtures are enabled.
In production, connections are never shared across threads, as such this extra
synchronization is wasteful.
This change makes the initialization of the hash upon missing key fully thread-safe. Before this change, initialization that would occur in two threads could overwrite each other, as illustrated here:
https://github.com/ruby-concurrency/concurrent-ruby/issues/970
`ActiveSupport::ParameterFilter::CompiledFilter` was originally
extracted in 79e91cc0ec as a performance
optimization. Its purpose was to avoid redundantly checking
`@filters.empty?`. However, redundant checks can be avoided by using
separate `ParameterFilter` methods, rather than allocating a separate
`CompiledFilter` object. Therefore, this commit reintegrates
`CompiledFilter` back into `ParameterFilter`.
Additionally, lazy compilation of filters predates the extraction of
`ParameterFilter` from `ActionDispatch::Http::FilterParameters` in
e466354edb. However, since
`ActionDispatch::Http::FilterParameters` lazily allocates a
`ParameterFilter` instance, there is no benefit to `ParameterFilter`
also lazily compiling filters. Therefore, this commit switches from
lazy compilation to eager compilation.
Together, these changes yield a small performance increase:
**Benchmark script**
```ruby
# frozen_string_literal: true
require "benchmark/ips"
require "benchmark/memory"
require "active_support"
require "active_support/parameter_filter"
ootb = [:passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn]
mixed = [:passw, "secret", /token/, :crypt, "salt", /certificate/, "user.otp", /user\.ssn/, proc {}]
params = {
"user" => {
"name" => :name,
"email" => :email,
"password" => :password,
"ssn" => :ssn,
"locations" => [
{ "city" => :city, "country" => :country },
{ "city" => :city, "country" => :country },
],
}
}
Benchmark.ips do |x|
x.report("ootb") do
ActiveSupport::ParameterFilter.new(ootb).filter(params)
end
x.report("mixed") do
ActiveSupport::ParameterFilter.new(mixed).filter(params)
end
end
```
**Before**
```
Warming up --------------------------------------
ootb 2.032k i/100ms
mixed 1.521k i/100ms
Calculating -------------------------------------
ootb 20.315k (± 1.3%) i/s - 101.600k in 5.001939s
mixed 15.142k (± 1.2%) i/s - 76.050k in 5.023077s
```
**After**
```
Warming up --------------------------------------
ootb 2.163k i/100ms
mixed 1.604k i/100ms
Calculating -------------------------------------
ootb 21.478k (± 1.2%) i/s - 108.150k in 5.036188s
mixed 16.052k (± 0.8%) i/s - 81.804k in 5.096656s
```
`Forking.run_in_isolation` opens two ends of a pipe. The fork process closes
the read end, writes to it, and then terminates (which presumably closes the
file descriptors on its end). The parent process closes the write end, reads
from it, and returns, never closing the read end.
This results in an accumulation of open file descriptors, which can cause
errors if the limit is reached.
One approach to fixing this would be to simply close the read end of the pipe
in the parent process. However, it is more idiomatic to open the pipe given a
block, which automatically closes the pipe after the block exits.
This commit adds `ActiveSupport.deprecator` and replaces all usages of
`ActiveSupport::Deprecation.warn` in `activesupport/lib` with
`ActiveSupport.deprecator`.
Additionally, this commit adds `ActiveSupport.deprecator` to
`Rails.application.deprecators` so that it can be configured using e.g.
`config.active_support.report_deprecations`.
This commits adds a deprecator-only overload for `assert_deprecated`.
For example, `assert_deprecated(/./, deprecator)` can now be written as
`assert_deprecated(deprecator)`.
This is part of an initiative to move away from the top-level
`ActiveSupport::Deprecation` API, towards per-library deprecator
instances. See also #46000 and its follow-ups.
This applies the following configs to `Rails.application.deprecators`,
which then applies them to `ActiveSupport::Deprecation.instance`:
* `config.active_support.report_deprecations`
* `config.active_support.deprecation`
* `config.active_support.disallowed_deprecation`
* `config.active_support.disallowed_deprecation_warnings`
This test was added in 308e84e to ensure that calling #silence on a
broadcasted logger works if the broadcastee implements #silence but the
broadcaster does not.
However, `include ActiveSupport::LoggerSilence` was moved from the
`FakeLogger` to the `CustomLogger` in 05ad44e. This resulted in both
loggers in the above test implementing silence, so it no longer tested
the behavior expected (that only one of them implements #silence).
The inclusion of `LoggerSilence` was moved back to `FakeLogger` so that
`CustomLogger` does not implement silence, and assertions were added so
that this does not regress. In addition, methods transitively included
by `LoggerSilence` are overriden so that they continue to behave like
`CustomLogger`.
Prior to this commit, when `Time#change` or `Time#advance` constructed a
time inside the final stretch of Daylight Saving Time (DST), the non-DST
offset would always be chosen for local times:
```ruby
# DST ended just before 2021-11-07 2:00:00 AM in US/Eastern.
ENV["TZ"] = "US/Eastern"
time = Time.local(2021, 11, 07, 00, 59, 59) + 1
# => 2021-11-07 01:00:00 -0400
time.change(day: 07)
# => 2021-11-07 01:00:00 -0500
time.advance(seconds: 0)
# => 2021-11-07 01:00:00 -0500
time = Time.local(2021, 11, 06, 01, 00, 00)
# => 2021-11-06 01:00:00 -0400
time.change(day: 07)
# => 2021-11-07 01:00:00 -0500
time.advance(days: 1)
# => 2021-11-07 01:00:00 -0500
```
And the DST offset would always be chosen for times with a `TimeZone`
object:
```ruby
Time.zone = "US/Eastern"
time = Time.new(2021, 11, 07, 02, 00, 00, Time.zone) - 3600
# => 2021-11-07 01:00:00 -0500
time.change(day: 07)
# => 2021-11-07 01:00:00 -0400
time.advance(seconds: 0)
# => 2021-11-07 01:00:00 -0400
time = Time.new(2021, 11, 8, 01, 00, 00, Time.zone)
# => 2021-11-08 01:00:00 -0500
time.change(day: 07)
# => 2021-11-07 01:00:00 -0400
time.advance(days: -1)
# => 2021-11-07 01:00:00 -0400
```
This commit fixes `Time#change` and `Time#advance` to choose the offset
that matches the original time's offset when possible:
```ruby
ENV["TZ"] = "US/Eastern"
time = Time.local(2021, 11, 07, 00, 59, 59) + 1
# => 2021-11-07 01:00:00 -0400
time.change(day: 07)
# => 2021-11-07 01:00:00 -0400
time.advance(seconds: 0)
# => 2021-11-07 01:00:00 -0400
time = Time.local(2021, 11, 06, 01, 00, 00)
# => 2021-11-06 01:00:00 -0400
time.change(day: 07)
# => 2021-11-07 01:00:00 -0400
time.advance(days: 1)
# => 2021-11-07 01:00:00 -0400
Time.zone = "US/Eastern"
time = Time.new(2021, 11, 07, 02, 00, 00, Time.zone) - 3600
# => 2021-11-07 01:00:00 -0500
time.change(day: 07)
# => 2021-11-07 01:00:00 -0500
time.advance(seconds: 0)
# => 2021-11-07 01:00:00 -0500
time = Time.new(2021, 11, 8, 01, 00, 00, Time.zone)
# => 2021-11-08 01:00:00 -0500
time.change(day: 07)
# => 2021-11-07 01:00:00 -0500
time.advance(days: -1)
# => 2021-11-07 01:00:00 -0500
```
It's worth noting that there is a somewhat dubious case when calling
`advance` with a mix of calendar and clock units (e.g. months + hours).
`advance` adds calendar units first, then adds clock units. So a
non-DST time may first be advanced to a date within DST before any clock
units are applied. For example:
```ruby
# DST began on 2021-03-14 in US/Eastern.
Time.zone = "US/Eastern"
non_dst = Time.new(2021, 03, 07, 00, 00, 00, Time.zone)
# => 2021-03-07 00:00:00 -0500
non_dst.advance(months: 8, hours: 1)
# => 2021-11-07 01:00:00 -0400
```
One could argue that the expected result is `2021-11-07 01:00:00 -0500`.
However, the difference between that and the result for `hours: 0` is
greater than 1 hour:
```ruby
adjusted_result = non_dst.advance(months: 8, hours: 1) + 3600
# => 2021-11-07 01:00:00 -0500
adjusted_result - non_dst.advance(months: 8, hours: 0)
# => 7200.0
```
Which might also be unexpected.
Furthermore, it may be difficult (or expensive) to apply such an
adjustment in a consistent way. For example, the result for `hours: 2`
does have the expected `-0500` offset, so it might seem no adjustment is
necessary, but if we didn't adjust it too, it would conflict with the
adjusted `hours: 1` result:
```ruby
non_dst.advance(months: 8, hours: 2)
# => 2021-11-07 01:00:00 -0500
```
Therefore, the approach in this commit (which produces a `-0400` offset
for `hours: 1`) seems like a reasonable compromise.
Fixes#45055.
Closes#45556.
Closes#46248.
Co-authored-by: Kevin Hall <bigtoe416@yahoo.com>
Co-authored-by: Takayoshi Nishida <takayoshi.nishida@gmail.com>
Fix: https://github.com/rails/rails/issues/46301
When incrementing in Memcached or Redis, we use `raw` values, so
only the backing store TTL counts, and is preserved.
So for consistency we need to emulate this in MemoryStore by
re-using the existing entry's `expires_at`.
`Rails.error.handle` and `Rails.error.record` are able to filter by list of serveral error classes now, for example like this:
```ruby
Rails.error.handle(ArgumentError, TypeError) do
[1, 2, 3].first(x) # where `x` might be `-4` or `'4'`
end
```
We recently let a few very easy to avoid warnings get merged.
The root cause is that locally the test suite doesn't run in
verbose mode unless you explictly pass `-w`.
On CI warnings are enabled, but there is no reason to look at the
build output unless something is failing. And even if one wanted
to do that, that would be particularly work intensive since warnings
may be specific to a Ruby version etc.
Because of this I believe we should:
- Always run the test suite with warnings enabled.
- Raise an error if a warning is unexpected.
We've been using this pattern for a long time at Shopify both in private
and public repositories.
This commit maps the column information returned from ErrorHighlight in
to column information within the source ERB template. ErrorHighlight
only understands the compiled Ruby code, so this commit adds a small
translation layer that converts the values from ErrorHighlight in to the
right values for the ERB source template
We should get out of the business of parsing backtraces and only use
backtrace locations. Backtrace locations have the file and line number
information baked in, so we don't need to parse things anymore
This commit adds a SyntaxErrorProxy object to active support and wraps
syntax error exceptions with that proxy object. We want to enhance
syntax errors with information about the source location where they
actually happened (normally the backtrace doesn't contain such info).
Rather than mutating the original exception's backtrace, this wraps it
with a proxy object.
Eventually we will implement backtrace_locations on the proxy object so
that the exception handling middleware can be updated to _only_ deal
with backtrace_locations and never deal with raw `backtrace`
This adds a `Rails.application.deprecators` method that returns a
managed collection of deprecators.
Individual deprecators can be added and retrieved from the collection:
```ruby
Rails.application.deprecators[:my_gem] = ActiveSupport::Deprecation.new("2.0", "MyGem")
Rails.application.deprecators[:other_gem] = ActiveSupport::Deprecation.new("3.0", "OtherGem")
```
And the collection's configuration methods affect all deprecators in the
collection:
```ruby
Rails.application.deprecators.debug = true
Rails.application.deprecators[:my_gem].debug
# => true
Rails.application.deprecators[:other_gem].debug
# => true
```
Additionally, all deprecators in the collection can be silenced for the
duration of a given block:
```ruby
Rails.application.deprecators.silence do
Rails.application.deprecators[:my_gem].warn # => silenced (no warning)
Rails.application.deprecators[:other_gem].warn # => silenced (no warning)
end
```
Previously you could only use a block, which was inconsistent with other callback APIs in Rails. Now, you can use a block or a method name. The block API is still recommended in the docs.
```ruby
class Current < ActiveSupport::CurrentAttributes
resets { Time.zone = nil }
resets :clear_time_zone
end
```
This also works for `before_reset`.
```ruby
class Current < ActiveSupport::CurrentAttributes
before_reset { Time.zone = nil }
before_reset :clear_time_zone
end
```
It's a bit of a stab in the dark but I'm hoping it may fix
the flaky failures such as:
```ruby
Failure:
OptimizedMemCacheStoreTest#test_iso_8859_6_encoded_values [/rails/activesupport/test/cache/behaviors/encoded_key_cache_behavior.rb:10]:
Expected false to be truthy.
```
The CHANGELOG entry for #44179 originally focused on
`ActiveSupport::MessageVerifiers` and `ActiveSupport::MessageEncryptors`,
and made sense in `activesupport/CHANGELOG.md`. However, prior to
merge, the focus changed to `Rails.application.message_verifiers`.
Therefore, the CHANGELOG entry should be in `railties/CHANGELOG.md`.
When calling `#descendants` on a non-reloadable class with reloadable
descendants, it can return classes that were unloaded but not yet
garbage collected.
`DescendantsTracker` have been dealing with this for a long time
but the core_ext versions of these methods were never made reloader
aware.
This also refactor `DescendantsTracker` to not be so different when
there is no native `#subclasses` method.