Commit 0206476a authored by Sean McGivern's avatar Sean McGivern

Fix some N+1s when calculating notification recipients

First N+1: we may have loaded a user's notification settings already, but not
have loaded their sources. Because we're iterating through, we'd potentially
load sources that are completely unrelated, just because they belong to this
user.

Second N+1: we do a separate query for each user who could be subscribed to or
unsubcribed from the target. It's actually more efficient in this case to get
all subscriptions at once, as we will need to check most of them.

We can fix both by the slightly unpleasant means of checking IDs manually,
rather than object equality.
parent 95b3e51f
...@@ -64,7 +64,7 @@ class NotificationRecipient ...@@ -64,7 +64,7 @@ class NotificationRecipient
return false unless @target return false unless @target
return false unless @target.respond_to?(:subscriptions) return false unless @target.respond_to?(:subscriptions)
subscription = @target.subscriptions.find_by_user_id(@user.id) subscription = @target.subscriptions.find { |subscription| subscription.user_id == @user.id }
subscription && !subscription.subscribed subscription && !subscription.subscribed
end end
......
...@@ -1038,7 +1038,10 @@ class User < ActiveRecord::Base ...@@ -1038,7 +1038,10 @@ class User < ActiveRecord::Base
def notification_settings_for(source) def notification_settings_for(source)
if notification_settings.loaded? if notification_settings.loaded?
notification_settings.find { |notification| notification.source == source } notification_settings.find do |notification|
notification.source_type == source.class.base_class.name &&
notification.source_id == source.id
end
else else
notification_settings.find_or_initialize_by(source: source) notification_settings.find_or_initialize_by(source: source)
end end
......
---
title: Fix some sources of excessive query counts when calculating notification recipients
merge_request:
author:
type: performance
require 'spec_helper'
describe NotificationRecipientService do
let(:service) { described_class }
let(:assignee) { create(:user) }
let(:project) { create(:project, :public) }
let(:other_projects) { create_list(:project, 5, :public) }
describe '#build_new_note_recipients' do
let(:issue) { create(:issue, project: project, assignees: [assignee]) }
let(:note) { create(:note_on_issue, noteable: issue, project_id: issue.project_id) }
def create_watcher
watcher = create(:user)
create(:notification_setting, project: project, user: watcher, level: :watch)
other_projects.each do |other_project|
create(:notification_setting, project: other_project, user: watcher, level: :watch)
end
end
it 'avoids N+1 queries' do
create_watcher
service.build_new_note_recipients(note)
control_count = ActiveRecord::QueryRecorder.new do
service.build_new_note_recipients(note)
end
create_watcher
expect { service.build_new_note_recipients(note) }.not_to exceed_query_limit(control_count)
end
end
end
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