Skip to content

Use UPSERT when storing user mentions

Heinrich Lee Yu requested to merge 338729-use-upsert-for-storing-mentions into master

What does this MR do?

Using UPSERT saves us a query, avoids cases where we get RecordNotUnique, and removes the use of SAVEPOINT which can cause PG performance issues.

Old code

  1. Find the associated xxx_user_mentions record (1 query)
  2. If the new / updated Markdown has mentions, save the record. If not, destroy the record if it exists. (1 query)
  3. If we're creating a new record in step 2, it could raise RecordNotUnique in certain race conditions. If that happens, we fetch the record and do an update (2 queries)

This uses safe_ensure_unique which uses SAVEPOINT so that we can catch the uniqueness violation and not fail the whole transaction.

New code

  1. If the new / updated Markdown has mentions, upsert the record. If not, execute a DELETE query. (1 query)

The upsert query looks like this when creating / updating a note:

INSERT INTO "issue_user_mentions" 
  ("mentioned_users_ids", "mentioned_groups_ids", "mentioned_projects_ids", "issue_id", "note_id")
VALUES 
  ('{1}', NULL, NULL, 1, 1)
ON CONFLICT ("issue_id", "note_id")
DO UPDATE SET 
  "mentioned_users_ids"=excluded."mentioned_users_ids",
  "mentioned_groups_ids"=excluded."mentioned_groups_ids",
  "mentioned_projects_ids"=excluded."mentioned_projects_ids"
RETURNING "id"

When creating an issue / updating the description:

INSERT INTO "issue_user_mentions" 
  ("mentioned_users_ids", "mentioned_groups_ids", "mentioned_projects_ids", "issue_id", "note_id")
VALUES 
  ('{1}', NULL, NULL, 1, NULL)
ON CONFLICT ("issue_id") WHERE (note_id IS NULL)
DO UPDATE SET 
  "mentioned_users_ids"=excluded."mentioned_users_ids",
  "mentioned_groups_ids"=excluded."mentioned_groups_ids",
  "mentioned_projects_ids"=excluded."mentioned_projects_ids",
  "note_id"=excluded."note_id"
RETURNING "id"

Migration output

Up
== 20210818055357 AddUniqueCommitDesignUserMentionIndexes: migrating ==========
-- transaction_open?()
   -> 0.0000s
-- index_exists?(:commit_user_mentions, [:commit_id, :note_id], {:unique=>true, :name=>"commit_user_mentions_on_commit_id_and_note_id_unique_index", :algorithm=>:concurrently})
   -> 0.0043s
-- add_index(:commit_user_mentions, [:commit_id, :note_id], {:unique=>true, :name=>"commit_user_mentions_on_commit_id_and_note_id_unique_index", :algorithm=>:concurrently})
   -> 0.0052s
-- transaction_open?()
   -> 0.0000s
-- index_exists?(:design_user_mentions, [:design_id, :note_id], {:unique=>true, :name=>"design_user_mentions_on_design_id_and_note_id_unique_index", :algorithm=>:concurrently})
   -> 0.0024s
-- add_index(:design_user_mentions, [:design_id, :note_id], {:unique=>true, :name=>"design_user_mentions_on_design_id_and_note_id_unique_index", :algorithm=>:concurrently})
   -> 0.0053s
-- transaction_open?()
   -> 0.0000s
-- indexes(:commit_user_mentions)
   -> 0.0029s
-- remove_index(:commit_user_mentions, {:algorithm=>:concurrently, :name=>"commit_id_and_note_id_index"})
   -> 0.0028s
-- transaction_open?()
   -> 0.0000s
-- indexes(:design_user_mentions)
   -> 0.0026s
-- remove_index(:design_user_mentions, {:algorithm=>:concurrently, :name=>"design_user_mentions_on_design_id_and_note_id_index"})
   -> 0.0027s
== 20210818055357 AddUniqueCommitDesignUserMentionIndexes: migrated (0.0378s) =
Down
== 20210818055357 AddUniqueCommitDesignUserMentionIndexes: reverting ==========
-- transaction_open?()
   -> 0.0000s
-- index_exists?(:design_user_mentions, [:design_id, :note_id], {:name=>"design_user_mentions_on_design_id_and_note_id_index", :algorithm=>:concurrently})
   -> 0.0048s
-- add_index(:design_user_mentions, [:design_id, :note_id], {:name=>"design_user_mentions_on_design_id_and_note_id_index", :algorithm=>:concurrently})
   -> 0.0049s
-- transaction_open?()
   -> 0.0000s
-- index_exists?(:commit_user_mentions, [:commit_id, :note_id], {:name=>"commit_id_and_note_id_index", :algorithm=>:concurrently})
   -> 0.0023s
-- add_index(:commit_user_mentions, [:commit_id, :note_id], {:name=>"commit_id_and_note_id_index", :algorithm=>:concurrently})
   -> 0.0038s
-- transaction_open?()
   -> 0.0000s
-- indexes(:design_user_mentions)
   -> 0.0026s
-- remove_index(:design_user_mentions, {:algorithm=>:concurrently, :name=>"design_user_mentions_on_design_id_and_note_id_unique_index"})
   -> 0.0024s
-- transaction_open?()
   -> 0.0000s
-- indexes(:commit_user_mentions)
   -> 0.0031s
-- remove_index(:commit_user_mentions, {:algorithm=>:concurrently, :name=>"commit_user_mentions_on_commit_id_and_note_id_unique_index"})
   -> 0.0024s
== 20210818055357 AddUniqueCommitDesignUserMentionIndexes: reverted (0.0357s) =

How to setup and validate locally (strongly suggested)

  1. You can edit / create Markdown on the console and see the queries being generated. For example:
[1] pry(main)> i = Issue.first
=> #<Issue id:1 gitlab-org/gitlab-test#1>
[2] pry(main)> i.description = '@root test'
=> "@root test"
[3] pry(main)> i.save
...
  IssueUserMention Upsert (0.4ms)  INSERT INTO "issue_user_mentions" ("mentioned_users_ids","mentioned_groups_ids","mentioned_projects_ids","issue_id","note_id") VALUES ('{1}', NULL, NULL, 1, NULL) ON CONFLICT ("issue_id") WHERE (note_id IS NULL) DO UPDATE SET "mentioned_users_ids"=excluded."mentioned_users_ids","mentioned_groups_ids"=excluded."mentioned_groups_ids","mentioned_projects_ids"=excluded."mentioned_projects_ids","note_id"=excluded."note_id" RETURNING "id" /*application:console,line:/data/cache/bundle-2.7.2/ruby/2.7.0/gems/activerecord-6.1.3.2/lib/active_record/connection_adapters/postgresql/database_statements.rb:53:in `exec_query'*/
...

Does this MR meet the acceptance criteria?

Conformity

Availability and Testing

Security

Does this MR contain changes to processing or storing of credentials or tokens, authorization and authentication methods or other items described in the security review guidelines? If not, then delete this Security section.

  • Label as security and @ mention @gitlab-com/gl-security/appsec
  • The MR includes necessary changes to maintain consistency between UI, API, email, or other methods
  • Security reports checked/validated by a reviewer from the AppSec team

Related to #338729 (closed)

Edited by Heinrich Lee Yu

Merge request reports