`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.
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
```
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
```
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.
This is done by setting a special instance variable on the exception.
Another implementation could be to use an `ObjectSpace::WeakMap` as
a weak reference set, but I figured 3rd party error reporting libraries
may want to inspect wether the error was reported.
Currently, `Rails.application.message_verifier(name)` returns a
`MessageVerifier` instance using a secret derived from the given `name`.
Instances created this way always generate messages using the default
options for `MessageVerifier.new`. Also, if there are older options to
rotate for message verification, each instance created this way must be
configured individually. In cases where Rails itself uses
`Rails.application.message_verifier` (e.g. in Active Storage),
discovering the name of the instance may require digging through
Rails' source code.
To alleviate these issues, this commit adds a `MessageVerifiers` factory
class, which acts as a central point for configuring and creating
`MessageVerifier` instances. Options passed to `MessageVerifiers#rotate`
will be applied to `MessageVerifier` instances that `MessageVerifiers#[]`
creates. So the following:
```ruby
foo_verifier = Rails.application.message_verifier(:foo)
foo_verifier.rotate(old_options)
bar_verifier = Rails.application.message_verifier(:bar)
bar_verifier.rotate(old_options)
```
Can be rewritten as:
```ruby
Rails.application.message_verifiers.rotate(old_options)
foo_verifier = Rails.application.message_verifiers[:foo]
bar_verifier = Rails.application.message_verifiers[:bar]
```
Additionally, `Rails.application.message_verifiers` supports a
`:secret_key_base` option, so old `secret_key_base` values can be
rotated:
```ruby
Rails.application.message_verifiers.rotate(secret_key_base: "old secret_key_base")
```
`MessageVerifiers` memoizes `MessageVerifier` instances. This also
allows `MessageVerifier` instances to be overridden entirely:
```ruby
Rails.application.message_verifiers[:foo] = ActiveSupport::MessageVerifier.new(other_secret)
```
For parity, this commit also adds a `MessageEncryptors` factory class,
which fulfills the same role for `MessageEncryptor` instances.
This commit adds support for 3-arity `ActiveSupport::Deprecation`
behavior callbacks, which receive the deprecator instance as their third
argument. This allows such callbacks to change their behavior based on
the deprecator's state. For example, based on the deprecator's `debug`
flag.
This commit also fixes the `:stderr` and `:log` behaviors to check
the given deprecator's `debug` flag, instead of the global
`ActiveSupport::Deprecation.debug` flag.
Note that 2-arity and 4-arity callbacks will continue to behave the
same as before. However, callbacks with -1 and -2 arity (i.e. with
splat args) will now receive the deprecator as their third argument,
instead of the deprecation horizon and gem name.
Prior to this commit, the globally configured
`ActiveSupport::Deprecation.disallowed_warnings` affected all
deprecators, and each deprecator's `#disallowed_warnings` had no effect.
(Though the same was not true for `ActiveSupport::Deprecation.allow`,
which would only affect warnings from `ActiveSupport::Deprecation.warn`,
not warnings from non-global deprecators.)
This commit fixes `disallowed_warnings` to only affect the deprecator
on which it is configured.
Previously, only bold text was supported via a positional argument.
This allows for bold, italic, and underline options to be specified
for colored logs.
```ruby
info color("Hello world!", :red, bold: true, underline: true)
```
Rails 5 added String#upcase_first, which converts the first letter of a
string to uppercase (returning a duplicate). This commit adds the
corollary method, converting the first letter to lowercase.
This provides a basic level of protection against different threads
trying to mutate a shared default object. It is not a bulletproof
solution, because the default may contain nested non-frozen objects, but
it should cover common cases.
Closes: https://github.com/rails/rails/pull/45080
This is a surprisingly common use case when dealing with APIs
with short lived authentication tokens.
This implementation is very much the same as the original one
by Andrii, except that we yield an object with a restricted interface
instead of yielding the options hash.
The reason is that not all options can be changed between the read
and the write, so by restricting the interface we avoid surprising
users.
Co-Authored-By: Andrii Gladkyi <arg@arg.zone>
This makes the value supplied to the `default` option of
`thread_mattr_accessor` to be set in descendant classes
as well as in any new Thread that starts.
Previously, the `default` value provided was set only at the
moment of defining the attribute writer, which would cause
the attribute to be uninitialized in descendants and in other threads.
For instance:
```ruby
class Processor
thread_mattr_accessor :mode, default: :smart
end
class SubProcessor < Processor
end
SubProcessor.mode # => :smart (was `nil` prior to this commit)
Thread.new do
Processor.mode # => :smart (was `nil` prior to this commit)
end.join
```
If a non-`nil` default has been specified, there is a small (~7%)
performance decrease when reading non-`nil` values, and a larger (~45%)
performance decrease when reading `nil` values.
Benchmark script:
```ruby
# frozen_string_literal: true
require "benchmark/ips"
require "active_support"
require "active_support/core_ext/module/attribute_accessors_per_thread"
class MyClass
thread_mattr_accessor :default_value, default: "default"
thread_mattr_accessor :string_value, default: "default"
thread_mattr_accessor :nil_value, default: "default"
end
MyClass.string_value = "string"
MyClass.nil_value = nil
Benchmark.ips do |x|
x.report("default_value") { MyClass.default_value }
x.report("string_value") { MyClass.string_value }
x.report("nil_value") { MyClass.nil_value }
end
```
Before this commit:
```
default_value 2.075M (± 0.7%) i/s - 10.396M in 5.010585s
string_value 2.103M (± 0.7%) i/s - 10.672M in 5.074624s
nil_value 1.777M (± 0.9%) i/s - 8.924M in 5.023058s
```
After this commit:
```
default_value 2.008M (± 0.7%) i/s - 10.187M in 5.072990s
string_value 1.967M (± 0.7%) i/s - 9.891M in 5.028570s
nil_value 1.144M (± 0.5%) i/s - 5.770M in 5.041630s
```
If no default or a `nil` default is specified, there is no performance
impact.
Fixes#43312.
Co-authored-by: Jonathan Hefner <jonathan@hefner.pro>
Although Ruby provides `Base64.urlsafe_encode64` and
`Base64.urlsafe_decode64` methods, the technical term is "URL-safe",
with "URL" and "safe" as separate words.
For better readability, this commit renames the `:urlsafe` option for
`MessageEncryptor` and `MessageVerifier` to `:url_safe`.
with some additional changes made later:
1. Flip the condition for backward compatibility
Co-authored-by: Jonathan Hefner <jonathan@hefner.pro>
2. Improve custom behavior test
Co-authored-by: Jonathan Hefner <jonathan@hefner.pro>
3. Fix indentation
This adds a `:urlsafe` option to the `MessageEncryptor` constructor.
When enabled, this option ensures that messages use a URL-safe encoding.
This matches the `MessageVerifier` `:urlsafe` option added in #45419.
MessageVerifier uses Base64.strict_encode64 and generated strings are
not urlsafe. Though the goal is to make MessageVerifier generated
strings urlsafe, We can not simply switch to Base64.urlsafe_encode64
because it will be a breaking change. Thus, as a first step, urlsafe
option is added to the MessageVerifier initializer.
Add the method ERB::Util.xml_name_escape to escape dangerous characters
in names of tags and names of attributes, following the specification of
XML.
Use that method in the tag helpers of ActionView::Helpers. Rename the option
:escape_attributes to :escape, to simplify by applying the option to the whole
tag.
The stdlib Logger::new allows passing a :formatter keyword argument to
set the logger's formatter. ActiveSupport::Logger::new ignores this
argument by always setting the formatter to an instance of
SimpleFormatter. Instead, we should only set it when none is yet set.
With Ruby 2.4+ the default for +to_time+ changed from converting to the
local system time to preserving the offset of the receiver. At the time
Rails supported older versions of Ruby so a compatibility layer was
added to assist in the migration process. From Rails 5.0 new applications
have defaulted to the Ruby 2.4+ behavior and since Rails 7.0 now only
supports Ruby 2.7+ this compatibility layer can be safely removed.
To minimize any noise generated the deprecation warning only appears when
the setting is configured to `false` as that is the only scenario where
the removal of the compatibility layer has any effect.
Fix: https://github.com/rails/rails/issues/44452
We don't ignore blank path strings because as surprising as it is, they
are valid paths:
```ruby
>> path = Pathname.new(" ")
=> #<Pathname: >
>> path.write("test")
=> 4
>> path.exist?
=> true
```
Previously it would end up calling `Pathname#empty?` which returned true
if the path existed and was an empty directory or file.
That behavior was unlikely to be expected.
These changes include adding a hybrid serializer class
named JsonWithMarshalFallback in order for existing apps
to have an upgrade path from Marshal to JSON.
Since `#subclasses` was introduced a bit after `#descendants`, the feature testing
code assumed that if the former was present, the later would be too.
However it was decided to revert `#descendants` for now, but to keep `#subclasses`
https://bugs.ruby-lang.org/issues/14394#note-33
Since this was totally unexpected, the `#descendants` core_ext and `DescendantsTracker#descendants`
are broken on Active Support 7.0.0 / Ruby 3.1.0-dev.
We now test the existence of both methods to handle this new situation. Since
for now it's planned for `#descendants` to make it back in Ruby 3.2, we keep
checking for it.
* bug: illustrate negative durations don't add to the same time
* fix: equivalent negative durations add to the same time
Co-authored-by: Caleb <me@cpb.ca>
Co-authored-by: Braden Staudacher <braden.staudacher@chime.com>
* Updates CHANGELOG with fix to `ActiveSupport::Duration.build`