Attributes such as `set` and `reset` should not be used as they clash with the `CurrentAttributes` public API. This PR raises an `ArgumentError` if a restricted attribute name is used.
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
```
Many places in Active Support and Rails in general use `Thread.current#[]`
to store "request (or job) local data". This often cause problems with
`Enumerator` because it runs in a different fiber.
On the other hand, some places migrated to `Thread#thread_variable_get`
which cause issues with fiber based servers (`falcon`).
Based on this, I believe the isolation level should be an application
configuration.
For backward compatibility it could ship with `:fiber` isolation as a default
but longer term :thread would make more sense as it would work fine for
all deployment targets except falcon.
Ref: https://github.com/rails/rails/pull/38905
Ref: https://github.com/rails/rails/pull/39428
Ref: https://github.com/rails/rails/pull/34495
(and possibly many others)
The bulk of the optimization is to generate code rather than use
`define_method` with a closure.
```
Warming up --------------------------------------
original 207.468k i/100ms
code-generator 340.849k i/100ms
Calculating -------------------------------------
original 2.127M (± 1.1%) i/s - 10.788M in 5.073860s
code-generator 3.426M (± 0.9%) i/s - 17.383M in 5.073965s
Comparison:
code-generator: 3426241.0 i/s
original: 2126539.2 i/s - 1.61x (± 0.00) slower
```
```ruby
require 'benchmark/ips'
require 'active_support/all'
class Original < ActiveSupport::CurrentAttributes
attribute :foo
end
class CodeGen < ActiveSupport::CurrentAttributes
class << self
def attribute(*names)
ActiveSupport::CodeGenerator.batch(generated_attribute_methods, __FILE__, __LINE__) do |owner|
names.each do |name|
owner.define_cached_method(name, namespace: :current_attributes) do |batch|
batch <<
"def #{name}" <<
"attributes[:#{name}]" <<
"end"
end
owner.define_cached_method("#{name}=", namespace: :current_attributes) do |batch|
batch <<
"def #{name}=(value)" <<
"attributes[:#{name}] = value" <<
"end"
end
end
end
ActiveSupport::CodeGenerator.batch(singleton_class, __FILE__, __LINE__) do |owner|
names.each do |name|
owner.define_cached_method(name, namespace: :current_attributes_delegation) do |batch|
batch <<
"def #{name}" <<
"instance.#{name}" <<
"end"
end
owner.define_cached_method("#{name}=", namespace: :current_attributes_delegation) do |batch|
batch <<
"def #{name}=(value)" <<
"instance.#{name} = value" <<
"end"
end
end
end
end
end
attribute :foo
end
Benchmark.ips do |x|
x.report('original') { Original.foo }
x.report('code-generator') { CodeGen.foo }
x.compare!
end
```
The test for CurrentAttributes using thread-local variables leaks
an instance in Thread.current.
Usually instances are reset in the test helper but the objects remain:
activesupport/lib/active_support/current_attributes/test_helper.rb:11
In this situation we use clear_all to make sure we don't leave instances
behind when the configuration is changed.
This is useful when we need to do some work associated to `Current.reset`
but that work depends on the values of the current attributes themselves.
This cannot be done in the supported `resets` callback because when the
block is executed, CurrentAttributes's instance has already been reset.
For symmetry, `after_reset` is defined as alias of `resets`.
In some examples and guides we are recommending to use code like:
```ruby
verified_user = User.find_by(id: cookies.signed[:user_id])
```
My suggestion is to use instead:
```ruby
verified_user = User.find_by(id: cookies.encrypted[:user_id])
```
which invites users to prefer the "newer" encrypted cookies over the
"legacy" signed cookies.
If users added an attribute or otherwise changed a CurrentAttributes subclass
they'd see exceptions on the next page load.
Because `ActiveSupport::CurrentAttributes.current_instances` would keep
references to the old instances from the previous request.
We can fix this by clearing out the `current_attributes` before we unload
constants. Then any change to the model can be autoloaded again since its
slot isn't taken by an old instance.
We'll still have to call reset before we clear so external collaborators,
like Time.zone, won't linger with their current value throughout other code.
* Add ActiveSupport::CurrentAttributes to provide a thread-isolated attributes singleton
* Need to require first
* Move stubs into test namespace.
Thus they won't conflict with other Current and Person stubs.
* End of the line for you, whitespace!
* Support super in attribute methods.
Define instance level accessors in an included module such that
`super` in an overriden accessor works, akin to Active Model.
* Spare users the manual require.
Follow the example of concerns, autoload in the top level Active Support file.
* Add bidelegation support
* Rename #expose to #set. Simpler, clearer
* Automatically reset every instance.
Skips the need for users to actively embed something that resets
their CurrentAttributes instances.
* Fix test name; add tangible name value when blank.
* Try to ensure we run after a request as well.
* Delegate all missing methods to the instance
This allows regular `delegate` to serve, so we don't need bidelegate.
* Properly test resetting after execution cycle.
Also remove the stale puts debugging.
* Update documentation to match new autoreset