Skip to content

Commit cd100e5

Browse files
authored
Merge pull request rails#48053 from fatkodima/fix-find_or_create_by-transactions
Fix `find_or_create_by`/`find_or_create_by!` when used within concurrent transactions
2 parents 611dac6 + 7130e3c commit cd100e5

File tree

2 files changed

+60
-2
lines changed

2 files changed

+60
-2
lines changed

activerecord/lib/active_record/relation.rb

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,11 @@ def find_or_create_by!(attributes, &block)
215215
def create_or_find_by(attributes, &block)
216216
transaction(requires_new: true) { create(attributes, &block) }
217217
rescue ActiveRecord::RecordNotUnique
218-
find_by!(attributes)
218+
if connection.transaction_open?
219+
where(attributes).lock.find_by!(attributes)
220+
else
221+
find_by!(attributes)
222+
end
219223
end
220224

221225
# Like #create_or_find_by, but calls
@@ -224,7 +228,11 @@ def create_or_find_by(attributes, &block)
224228
def create_or_find_by!(attributes, &block)
225229
transaction(requires_new: true) { create!(attributes, &block) }
226230
rescue ActiveRecord::RecordNotUnique
227-
find_by!(attributes)
231+
if connection.transaction_open?
232+
where(attributes).lock.find_by!(attributes)
233+
else
234+
find_by!(attributes)
235+
end
228236
end
229237

230238
# Like #find_or_create_by, but calls {new}[rdoc-ref:Core#new]

activerecord/test/cases/relations_test.rb

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2452,3 +2452,53 @@ def custom_post_relation(alias_name = "omg_posts")
24522452
)
24532453
end
24542454
end
2455+
2456+
class CreateOrFindByWithinTransactions < ActiveRecord::TestCase
2457+
unless current_adapter?(:SQLite3Adapter)
2458+
self.use_transactional_tests = false
2459+
2460+
def teardown
2461+
Subscriber.delete_all
2462+
end
2463+
2464+
def test_multiple_find_or_create_by_within_transactions
2465+
duel { Subscriber.find_or_create_by(nick: "bob") }
2466+
end
2467+
2468+
def test_multiple_find_or_create_by_bang_within_transactions
2469+
duel { Subscriber.find_or_create_by!(nick: "bob") }
2470+
end
2471+
2472+
private
2473+
def duel
2474+
assert_nil Subscriber.find_by(nick: "bob")
2475+
2476+
a_wakeup = Concurrent::Event.new
2477+
b_wakeup = Concurrent::Event.new
2478+
2479+
a = Thread.new do
2480+
Subscriber.transaction do
2481+
a_wakeup.wait
2482+
yield
2483+
b_wakeup.set
2484+
end
2485+
end
2486+
2487+
b = Thread.new do
2488+
Subscriber.transaction do
2489+
# Read the record prematurely for MySQL REPEATABLE READ to kick in
2490+
Subscriber.find_by(nick: "bob")
2491+
2492+
a_wakeup.set
2493+
b_wakeup.wait
2494+
yield
2495+
end
2496+
end
2497+
2498+
a.join
2499+
b.join
2500+
2501+
assert_equal 1, Subscriber.where(nick: "bob").count
2502+
end
2503+
end
2504+
end

0 commit comments

Comments
 (0)