Commit 59e44d44 authored by Heinrich Lee Yu's avatar Heinrich Lee Yu

Use UPSERT when storing user mentions

Using UPSERT saves us a query, avoids cases where we get
RecordNotUnique, and removes the use of SAVEPOINT which can cause PG
performance issues.
parent c8fa2f9a
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
class Commit class Commit
extend ActiveModel::Naming extend ActiveModel::Naming
extend Gitlab::Cache::RequestCache extend Gitlab::Cache::RequestCache
extend Gitlab::Utils::Override
include ActiveModel::Conversion include ActiveModel::Conversion
include Noteable include Noteable
...@@ -327,7 +328,7 @@ class Commit ...@@ -327,7 +328,7 @@ class Commit
end end
def user_mentions def user_mentions
CommitUserMention.where(commit_id: self.id) user_mention_class.where(commit_id: self.id)
end end
def discussion_notes def discussion_notes
...@@ -554,6 +555,19 @@ class Commit ...@@ -554,6 +555,19 @@ class Commit
Ability.allowed?(user, :read_commit, self) Ability.allowed?(user, :read_commit, self)
end end
override :user_mention_class
def user_mention_class
CommitUserMention
end
override :user_mention_identifier
def user_mention_identifier
{
commit_id: id,
note_id: nil
}
end
private private
def expire_note_etag_cache_for_related_mrs def expire_note_etag_cache_for_related_mrs
......
...@@ -160,6 +160,12 @@ module CacheMarkdownField ...@@ -160,6 +160,12 @@ module CacheMarkdownField
# We can only store mentions if the mentionable is a database object # We can only store mentions if the mentionable is a database object
return unless self.is_a?(ApplicationRecord) return unless self.is_a?(ApplicationRecord)
identifier = user_mention_identifier
# this may happen due to notes polymorphism, so noteable_id may point to a record
# that no longer exists as we cannot have FK on noteable_id
return if identifier.blank?
refs = all_references(self.author) refs = all_references(self.author)
references = {} references = {}
...@@ -167,24 +173,10 @@ module CacheMarkdownField ...@@ -167,24 +173,10 @@ module CacheMarkdownField
references[:mentioned_groups_ids] = refs.mentioned_group_ids.presence references[:mentioned_groups_ids] = refs.mentioned_group_ids.presence
references[:mentioned_projects_ids] = refs.mentioned_project_ids.presence references[:mentioned_projects_ids] = refs.mentioned_project_ids.presence
# One retry is enough as next time `model_user_mention` should return the existing mention record, if references.compact.any?
# that threw the `ActiveRecord::RecordNotUnique` exception in first place. user_mention_class.upsert(references.merge(identifier), unique_by: identifier.compact.keys)
self.class.safe_ensure_unique(retries: 1) do else
user_mention = model_user_mention user_mention_class.delete_by(identifier)
# this may happen due to notes polymorphism, so noteable_id may point to a record
# that no longer exists as we cannot have FK on noteable_id
break if user_mention.blank?
user_mention.mentioned_users_ids = references[:mentioned_users_ids]
user_mention.mentioned_groups_ids = references[:mentioned_groups_ids]
user_mention.mentioned_projects_ids = references[:mentioned_projects_ids]
if user_mention.has_mentions?
user_mention.save!
else
user_mention.destroy!
end
end end
true true
......
...@@ -161,6 +161,21 @@ module Mentionable ...@@ -161,6 +161,21 @@ module Mentionable
create_cross_references!(author) create_cross_references!(author)
end end
def user_mention_class
user_mention_association.klass
end
# Identifier for the user mention that is parsed from model description rather then its related notes.
# Models that have a description attribute like Issue, MergeRequest, Epic, Snippet may have such a user mention.
# Other mentionable models like DesignManagement::Design, will never have such record as those do not have
# a description attribute.
def user_mention_identifier
{
user_mention_association.foreign_key => id,
note_id: nil
}
end
private private
def extracted_mentionables(refs) def extracted_mentionables(refs)
...@@ -199,15 +214,8 @@ module Mentionable ...@@ -199,15 +214,8 @@ module Mentionable
{} {}
end end
# User mention that is parsed from model description rather then its related notes. def user_mention_association
# Models that have a description attribute like Issue, MergeRequest, Epic, Snippet may have such a user mention. association(:user_mentions).reflection
# Other mentionable models like Commit, DesignManagement::Design, will never have such record as those do not have
# a description attribute.
#
# Using this method followed by a call to *save* may result in *ActiveRecord::RecordNotUnique* exception
# in a multi-threaded environment. Make sure to use it within a *safe_ensure_unique* block.
def model_user_mention
user_mentions.where(note_id: nil).first_or_initialize
end end
end end
......
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
# A note of this type is never resolvable. # A note of this type is never resolvable.
class Note < ApplicationRecord class Note < ApplicationRecord
extend ActiveModel::Naming extend ActiveModel::Naming
extend Gitlab::Utils::Override
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
include Participable include Participable
include Mentionable include Mentionable
...@@ -583,16 +585,24 @@ class Note < ApplicationRecord ...@@ -583,16 +585,24 @@ class Note < ApplicationRecord
cache_key_items.join(':') cache_key_items.join(':')
end end
private override :user_mention_class
def user_mention_class
return if noteable.blank?
noteable.user_mention_class
end
# Using this method followed by a call to *save* may result in *ActiveRecord::RecordNotUnique* exception override :user_mention_identifier
# in a multi-threaded environment. Make sure to use it within a *safe_ensure_unique* block. def user_mention_identifier
def model_user_mention return if noteable.blank?
return if user_mentions.is_a?(ActiveRecord::NullRelation)
user_mentions.first_or_initialize noteable.user_mention_identifier.merge({
note_id: id
})
end end
private
def system_note_viewable_by?(user) def system_note_viewable_by?(user)
return true unless system_note_metadata return true unless system_note_metadata
......
...@@ -517,7 +517,7 @@ RSpec.describe Issues::UpdateService, :mailer do ...@@ -517,7 +517,7 @@ RSpec.describe Issues::UpdateService, :mailer do
update_issue(description: "- [x] Task 1 #{user.to_reference}\n- [ ] Task 2 #{user.to_reference}") update_issue(description: "- [x] Task 1 #{user.to_reference}\n- [ ] Task 2 #{user.to_reference}")
end end
expect(recorded.count).to eq(baseline.count - 1) expect(recorded.count).to eq(baseline.count)
expect(recorded.cached_count).to eq(0) expect(recorded.cached_count).to eq(0)
end end
end end
......
...@@ -278,7 +278,7 @@ RSpec.shared_examples 'load mentions from DB' do |mentionable_type| ...@@ -278,7 +278,7 @@ RSpec.shared_examples 'load mentions from DB' do |mentionable_type|
context 'when stored user mention contains ids of inexistent records' do context 'when stored user mention contains ids of inexistent records' do
before do before do
user_mention = note.send(:model_user_mention) user_mention = note.user_mentions.first
mention_ids = { mention_ids = {
mentioned_users_ids: user_mention.mentioned_users_ids.to_a << non_existing_record_id, mentioned_users_ids: user_mention.mentioned_users_ids.to_a << non_existing_record_id,
mentioned_projects_ids: user_mention.mentioned_projects_ids.to_a << non_existing_record_id, mentioned_projects_ids: user_mention.mentioned_projects_ids.to_a << non_existing_record_id,
...@@ -302,7 +302,7 @@ RSpec.shared_examples 'load mentions from DB' do |mentionable_type| ...@@ -302,7 +302,7 @@ RSpec.shared_examples 'load mentions from DB' do |mentionable_type|
let(:group_member) { create(:group_member, user: create(:user), group: private_group) } let(:group_member) { create(:group_member, user: create(:user), group: private_group) }
before do before do
user_mention = note.send(:model_user_mention) user_mention = note.user_mentions.first
mention_ids = { mention_ids = {
mentioned_projects_ids: user_mention.mentioned_projects_ids.to_a << private_project.id, mentioned_projects_ids: user_mention.mentioned_projects_ids.to_a << private_project.id,
mentioned_groups_ids: user_mention.mentioned_groups_ids.to_a << private_group.id mentioned_groups_ids: user_mention.mentioned_groups_ids.to_a << private_group.id
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment