diff --git a/app/models/tag.rb b/app/models/tag.rb index dff10111123..c197aa80ee9 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_or_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 18378c000d2..0121ba33aee 100644 --- a/spec/models/tag_spec.rb +++ b/spec/models/tag_spec.rb @@ -261,6 +261,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 + Tag.find_or_create_by_names(i.zero? ? tag_name_upper : tag_name_lower) + end + end + + threads.each(&:join) + + tags = Tag.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')