From 1f9ddb7cf6fcf7a5193bd0b856c69e070323a807 Mon Sep 17 00:00:00 2001 From: Christian Oelschlegel <18003795+oelison@users.noreply.github.com> Date: Mon, 1 Dec 2025 15:56:52 +0100 Subject: [PATCH] fix(tag): prevent dupl. tags on concurrent inserts (#35792) Co-authored-by: Christian Oelschlegel Co-authored-by: Claire --- app/models/tag.rb | 10 ++++++++-- spec/models/tag_spec.rb | 21 +++++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/app/models/tag.rb b/app/models/tag.rb index f9eb6bfd33..c59b0f36a8 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -112,8 +112,14 @@ class Tag < ApplicationRecord names = Array(name_or_names).map { |str| [normalize(str), str] }.uniq(&:first) names.map do |(normalized_name, display_name)| - tag = matching_name(normalized_name).first || create(name: normalized_name, - display_name: display_name.gsub(HASHTAG_INVALID_CHARS_RE, '')) + tag = begin + matching_name(normalized_name).first || create!( + name: normalized_name, + display_name: display_name.gsub(HASHTAG_INVALID_CHARS_RE, '') + ) + rescue ActiveRecord::RecordNotUnique + find_normalized(normalized_name) + end yield tag if block_given? diff --git a/spec/models/tag_spec.rb b/spec/models/tag_spec.rb index d41d3a9e21..9a68ae36d6 100644 --- a/spec/models/tag_spec.rb +++ b/spec/models/tag_spec.rb @@ -265,6 +265,27 @@ RSpec.describe Tag do end end + describe '.find_or_create_by_names_race_condition' do + it 'handles simultaneous inserts of the same tag in different cases without error' do + tag_name_upper = 'Rails' + tag_name_lower = 'rails' + + threads = [] + + 2.times do |i| + threads << Thread.new do + described_class.find_or_create_by_names(i.zero? ? tag_name_upper : tag_name_lower) + end + end + + threads.each(&:join) + + tags = described_class.where('lower(name) = ?', tag_name_lower.downcase) + expect(tags.count).to eq(1) + expect(tags.first.name.downcase).to eq(tag_name_lower.downcase) + end + end + describe '.search_for' do it 'finds tag records with matching names' do tag = Fabricate(:tag, name: 'match')