Prevent CurrentAttributes defaults from leaking

Follow-up to #50677.

Prior to this commit, all `ActiveSupport::CurrentAttributes` subclasses
stored their default values in the same `Hash`, causing default values
to leak between classes.  This commit ensures each subclass maintains a
separate `Hash`.

This commit also simplifies the resolution of default values, replacing
the `merge_defaults!` method with `resolve_defaults`.
This commit is contained in:
Jonathan Hefner 2024-01-11 12:40:14 -06:00
parent c5f3a81430
commit aa98bc3c71
2 changed files with 14 additions and 13 deletions

@ -134,7 +134,7 @@ def attribute(*names, default: nil)
singleton_class.delegate(*names.flat_map { |name| [name, "#{name}="] }, to: :instance, as: self)
defaults.merge! names.index_with { default }
self.defaults = defaults.merge(names.index_with { default })
end
# Calls this callback before #reset is called on the instance. Used for resetting external collaborators that depend on current values.
@ -188,12 +188,12 @@ def method_added(name)
end
end
class_attribute :defaults, instance_writer: false, default: {}
class_attribute :defaults, instance_writer: false, default: {}.freeze
attr_accessor :attributes
def initialize
@attributes = merge_defaults!({})
@attributes = resolve_defaults
end
# Expose one or more attributes within a block. Old values are returned after the block concludes.
@ -213,20 +213,14 @@ def set(attributes, &block)
# Reset all attributes. Should be called before and after actions, when used as a per-request singleton.
def reset
run_callbacks :reset do
self.attributes = merge_defaults!({})
self.attributes = resolve_defaults
end
end
private
def merge_defaults!(attributes)
defaults.each_with_object(attributes) do |(name, default), values|
value =
case default
when Proc then default.call
else default.dup
end
values[name] = value
def resolve_defaults
defaults.transform_values do |value|
Proc === value ? value.call : value.dup
end
end
end

@ -214,6 +214,13 @@ def after_teardown
assert_equal true, Current.respond_to?("respond_to_test")
end
test "CurrentAttributes defaults do not leak between classes" do
Class.new(ActiveSupport::CurrentAttributes) { attribute :counter_integer, default: 100 }
Current.reset
assert_equal 0, Current.counter_integer
end
test "CurrentAttributes use fiber-local variables" do
previous_level = ActiveSupport::IsolatedExecutionState.isolation_level
ActiveSupport::IsolatedExecutionState.isolation_level = :fiber