Skip to content
Snippets Groups Projects
Commit 8044440d authored by Stan Hu's avatar Stan Hu
Browse files

Eliminate many Gitaly calls in discussions API

Previously, the API to retrieve discussions from merge requests often
generated hundreds of Gitaly calls to determine whether a system note
should be shown to the user. It did this by:

1. Rendering the Markdown
2. Extracting cross-references from the Markdown
3. For cross-references that were commits, a Gitaly FindCommit RPC
   would be issued to validate that the commit exists.

The last step is unnecessary because we don't need to display a commit
if the user doesn't have access to the project in the first place.

`RendersNotes#prepare_notes_for_rendering` is already used in
`MergeRequestsController`, which is why we don't see N+1 Gitaly calls
there. We use it here to optimize the note redaction process.

Closes https://gitlab.com/gitlab-org/gitlab-ce/issues/65957
parent 4c4bd2c4
No related branches found
No related tags found
No related merge requests found
---
title: Eliminate many Gitaly calls in discussions API
merge_request: 31834
author:
type: performance
Loading
Loading
@@ -4,6 +4,7 @@ module API
class Discussions < Grape::API
include PaginationParams
helpers ::API::Helpers::NotesHelpers
helpers ::RendersNotes
 
before { authenticate! }
 
Loading
Loading
@@ -23,21 +24,15 @@ module API
requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable'
use :pagination
end
# rubocop: disable CodeReuse/ActiveRecord
get ":id/#{noteables_path}/:noteable_id/discussions" do
noteable = find_noteable(parent_type, params[:id], noteable_type, params[:noteable_id])
 
notes = noteable.notes
.inc_relations_for_view
.includes(:noteable)
.fresh
notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) }
notes = readable_discussion_notes(noteable)
discussions = Kaminari.paginate_array(Discussion.build_collection(notes, noteable))
 
present paginate(discussions), with: Entities::Discussion
end
# rubocop: enable CodeReuse/ActiveRecord
 
desc "Get a single #{noteable_type.to_s.downcase} discussion" do
success Entities::Discussion
Loading
Loading
@@ -226,13 +221,24 @@ module API
 
helpers do
# rubocop: disable CodeReuse/ActiveRecord
def readable_discussion_notes(noteable, discussion_id)
def readable_discussion_notes(noteable, discussion_id = nil)
notes = noteable.notes
.where(discussion_id: discussion_id)
notes = notes.where(discussion_id: discussion_id) if discussion_id
notes = notes
.inc_relations_for_view
.includes(:noteable)
.fresh
 
# Without RendersActions#prepare_notes_for_rendering,
# Note#cross_reference_not_visible_for? will attempt to render
# Markdown references mentioned in the note to see whether they
# should be redacted. For notes that reference a commit, this
# would also incur a Gitaly call to verify the commit exists.
#
# With prepare_notes_for_rendering, we can avoid Gitaly calls
# because notes are redacted if they point to projects that
# cannot be accessed by the user.
notes = prepare_notes_for_rendering(notes)
notes.reject { |n| n.cross_reference_not_visible_for?(current_user) }
end
# rubocop: enable CodeReuse/ActiveRecord
Loading
Loading
Loading
Loading
@@ -9,6 +9,59 @@ describe API::Discussions do
project.add_developer(user)
end
 
context 'with cross-reference system notes', :request_store do
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.project }
let(:new_merge_request) { create(:merge_request) }
let(:commit) { new_merge_request.project.commit }
let!(:note) { create(:system_note, noteable: merge_request, project: project, note: cross_reference) }
let!(:note_metadata) { create(:system_note_metadata, note: note, action: 'cross_reference') }
let(:cross_reference) { "test commit #{commit.to_reference(project)}" }
let(:url) { "/projects/#{project.id}/merge_requests/#{merge_request.iid}/discussions" }
before do
project.add_developer(user)
new_merge_request.project.add_developer(user)
end
it 'returns only the note that the user should see' do
hidden_merge_request = create(:merge_request)
new_cross_reference = "test commit #{hidden_merge_request.project.commit}"
new_note = create(:system_note, noteable: merge_request, project: project, note: new_cross_reference)
create(:system_note_metadata, note: new_note, action: 'cross_reference')
get api(url, user)
expect(response).to have_gitlab_http_status(200)
expect(json_response.count).to eq(1)
expect(json_response.first['notes'].count).to eq(1)
parsed_note = json_response.first['notes'].first
expect(parsed_note['id']).to eq(note.id)
expect(parsed_note['body']).to eq(cross_reference)
expect(parsed_note['system']).to be true
end
it 'avoids Git calls and N+1 SQL queries' do
expect_any_instance_of(Repository).not_to receive(:find_commit).with(commit.id)
control = ActiveRecord::QueryRecorder.new do
get api(url, user)
end
expect(response).to have_gitlab_http_status(200)
RequestStore.clear!
new_note = create(:system_note, noteable: merge_request, project: project, note: cross_reference)
create(:system_note_metadata, note: new_note, action: 'cross_reference')
RequestStore.clear!
expect { get api(url, user) }.not_to exceed_query_limit(control)
expect(response).to have_gitlab_http_status(200)
end
end
context 'when noteable is an Issue' do
let!(:issue) { create(:issue, project: project, author: user) }
let!(:issue_note) { create(:discussion_note_on_issue, noteable: issue, project: project, author: user) }
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment