From fa0277509564a8d663fcd7878709ab4130b07f2f Mon Sep 17 00:00:00 2001
From: Dan Ungureanu <dan@ungureanu.me>
Date: Fri, 11 Jun 2021 03:55:50 +0300
Subject: [PATCH] PERF: Perform user filtering in SQL (#13358)

Notifying about a tag change sometimes resulted in loading a large
number of users in memory just to perform an exclusion. This commit
prefers to do inclusion (i.e. instead of exclude users X, do include
users in groups Y) and does it in SQL to avoid fetching unnecessary
data that is later discarded.
---
 app/jobs/regular/notify_tag_change.rb | 17 +++++++----------
 app/services/post_alerter.rb          | 10 +++++++---
 2 files changed, 14 insertions(+), 13 deletions(-)

diff --git a/app/jobs/regular/notify_tag_change.rb b/app/jobs/regular/notify_tag_change.rb
index 4b513dd53b7..e503287810c 100644
--- a/app/jobs/regular/notify_tag_change.rb
+++ b/app/jobs/regular/notify_tag_change.rb
@@ -7,23 +7,20 @@ module Jobs
 
       if post&.topic&.visible?
         post_alerter = PostAlerter.new
-        post_alerter.notify_post_users(post, excluded_users(args), include_topic_watchers: !post.topic.private_message?, include_category_watchers: false)
+        post_alerter.notify_post_users(post, User.where(id: args[:notified_user_ids]),
+          group_ids: all_tags_in_hidden_groups?(args) ? tag_group_ids(args) : nil,
+          include_topic_watchers: !post.topic.private_message?,
+          include_category_watchers: false
+        )
         post_alerter.notify_first_post_watchers(post, post_alerter.tag_watchers(post.topic))
       end
     end
 
     private
 
-    def excluded_users(args)
-      if !args[:diff_tags] || !all_tags_in_hidden_groups?(args)
-        return User.where(id: args[:notified_user_ids])
-      end
-      group_users_join = DB.sql_fragment("LEFT JOIN group_users ON group_users.user_id = users.id AND group_users.group_id IN (:group_ids)", group_ids: tag_group_ids(args))
-      condition = DB.sql_fragment("group_users.id IS NULL OR users.id IN (:notified_user_ids)", notified_user_ids: args[:notified_user_ids])
-      User.joins(group_users_join).where(condition)
-    end
-
     def all_tags_in_hidden_groups?(args)
+      return false if args[:diff_tags].blank?
+
       Tag
         .where(name: args[:diff_tags])
         .joins(tag_groups: :tag_group_permissions)
diff --git a/app/services/post_alerter.rb b/app/services/post_alerter.rb
index 5051070eda3..297a97257dd 100644
--- a/app/services/post_alerter.rb
+++ b/app/services/post_alerter.rb
@@ -651,13 +651,13 @@ class PostAlerter
     emails_to_skip_send
   end
 
-  def notify_post_users(post, notified, include_topic_watchers: true, include_category_watchers: true, include_tag_watchers: true, new_record: false)
+  def notify_post_users(post, notified, group_ids: nil, include_topic_watchers: true, include_category_watchers: true, include_tag_watchers: true, new_record: false)
     return unless post.topic
 
     warn_if_not_sidekiq
 
     condition = +<<~SQL
-      id IN (
+      users.id IN (
         SELECT id FROM users WHERE false
         /*topic*/
         /*category*/
@@ -711,12 +711,16 @@ class PostAlerter
       tag_ids: tag_ids
     )
 
+    if group_ids.present?
+      notify = notify.joins(:group_users).where("group_users.group_id IN (?)", group_ids)
+    end
+
     if post.topic.private_message?
       notify = notify.where(staged: false).staff
     end
 
     exclude_user_ids = notified.map(&:id)
-    notify = notify.where("id NOT IN (?)", exclude_user_ids) if exclude_user_ids.present?
+    notify = notify.where("users.id NOT IN (?)", exclude_user_ids) if exclude_user_ids.present?
 
     DiscourseEvent.trigger(:before_create_notifications_for_users, notify, post)