Skip to content

Commit ae69680

Browse files
authored
Merge pull request rails#53890 from byroot/fix-lazy-attr-method-definition-thread-safety
Fix lazy attribute method definitiont to be thread safe
2 parents ab3b87e + b458919 commit ae69680

File tree

2 files changed

+70
-17
lines changed

2 files changed

+70
-17
lines changed

activerecord/lib/active_record/attribute_methods.rb

+22-17
Original file line numberDiff line numberDiff line change
@@ -116,10 +116,11 @@ def define_attribute_methods # :nodoc:
116116
alias_attribute :id_value, :id if _has_attribute?("id")
117117
end
118118

119-
@attribute_methods_generated = true
120-
121119
generate_alias_attributes
120+
121+
@attribute_methods_generated = true
122122
end
123+
123124
true
124125
end
125126

@@ -472,23 +473,27 @@ def respond_to_missing?(name, include_private = false)
472473
end
473474

474475
def method_missing(name, ...)
475-
unless self.class.attribute_methods_generated?
476-
if self.class.method_defined?(name)
477-
# The method is explicitly defined in the model, but calls a generated
478-
# method with super. So we must resume the call chain at the right step.
479-
last_method = method(name)
480-
last_method = last_method.super_method while last_method.super_method
481-
self.class.define_attribute_methods
482-
if last_method.super_method
483-
return last_method.super_method.call(...)
484-
end
485-
elsif self.class.define_attribute_methods
486-
# Some attribute methods weren't generated yet, we retry the call
487-
return public_send(name, ...)
488-
end
476+
# We can't know whether some method was defined or not because
477+
# multiple thread might be concurrently be in this code path.
478+
# So the first one would define the methods and the others would
479+
# appear to already have them.
480+
self.class.define_attribute_methods
481+
482+
# So in all cases we must behave as if the method was just defined.
483+
method = begin
484+
self.class.public_instance_method(name)
485+
rescue NameError
486+
nil
489487
end
490488

491-
super
489+
# The method might be explicitly defined in the model, but call a generated
490+
# method with super. So we must resume the call chain at the right step.
491+
method = method.super_method while method && !method.owner.is_a?(GeneratedAttributeMethods)
492+
if method
493+
method.bind_call(self, ...)
494+
else
495+
super
496+
end
492497
end
493498

494499
def attribute_method?(attr_name)

activerecord/test/cases/attribute_methods_test.rb

+48
Original file line numberDiff line numberDiff line change
@@ -1143,6 +1143,54 @@ def name
11431143
assert_nothing_raised { klass.define_attribute_method("bar") }
11441144
end
11451145

1146+
test "#method_missing define methods on the fly in a thread safe way" do
1147+
topic_class = Class.new(ActiveRecord::Base) do
1148+
self.table_name = "topics"
1149+
end
1150+
1151+
topic = topic_class.new(title: "New topic")
1152+
topic_class.undefine_attribute_methods
1153+
def topic.method_missing(...)
1154+
sleep 0.1 # required to cause a race condition
1155+
super
1156+
end
1157+
1158+
threads = 5.times.map do
1159+
Thread.new do
1160+
assert_equal "New topic", topic.title
1161+
end
1162+
end
1163+
threads.each(&:join)
1164+
ensure
1165+
threads&.each(&:kill)
1166+
end
1167+
1168+
test "#method_missing define methods on the fly in a thread safe way, even when decorated" do
1169+
topic_class = Class.new(ActiveRecord::Base) do
1170+
self.table_name = "topics"
1171+
1172+
def title
1173+
"title:#{super}"
1174+
end
1175+
end
1176+
1177+
topic = topic_class.new(title: "New topic")
1178+
topic_class.undefine_attribute_methods
1179+
def topic.method_missing(...)
1180+
sleep 0.1 # required to cause a race condition
1181+
super
1182+
end
1183+
1184+
threads = 5.times.map do
1185+
Thread.new do
1186+
assert_equal "title:New topic", topic.title
1187+
end
1188+
end
1189+
threads.each(&:join)
1190+
ensure
1191+
threads&.each(&:kill)
1192+
end
1193+
11461194
test "read_attribute with nil should not asplode" do
11471195
assert_nil Topic.new.read_attribute(nil)
11481196
end

0 commit comments

Comments
 (0)