diff options
author | Stan Hu <stanhu@gmail.com> | 2019-08-14 21:50:52 -0700 |
---|---|---|
committer | Stan Hu <stanhu@gmail.com> | 2019-08-15 06:44:48 -0700 |
commit | 40f1bc7a14593163a4b19ba9f53dc38e3652bbd5 (patch) | |
tree | b5a4a9a95f4b8f9d34e579dbe76eb8a7584b47c5 | |
parent | 4c4bd2c4ee69fa7a0d89aee7749589179c1ef600 (diff) | |
download | gitlab-ce-sh-fix-discussions-api-perf.tar.gz |
Eliminate many Gitaly calls in discussions APIsh-fix-discussions-api-perf
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
-rw-r--r-- | changelogs/unreleased/sh-fix-discussions-api-perf.yml | 5 | ||||
-rw-r--r-- | lib/api/discussions.rb | 23 | ||||
-rw-r--r-- | spec/requests/api/discussions_spec.rb | 53 |
3 files changed, 73 insertions, 8 deletions
diff --git a/changelogs/unreleased/sh-fix-discussions-api-perf.yml b/changelogs/unreleased/sh-fix-discussions-api-perf.yml new file mode 100644 index 00000000000..8cdbbf03dab --- /dev/null +++ b/changelogs/unreleased/sh-fix-discussions-api-perf.yml @@ -0,0 +1,5 @@ +--- +title: Eliminate many Gitaly calls in discussions API +merge_request: 31834 +author: +type: performance diff --git a/lib/api/discussions.rb b/lib/api/discussions.rb index cc62ce22a1b..296489283f0 100644 --- a/lib/api/discussions.rb +++ b/lib/api/discussions.rb @@ -4,6 +4,7 @@ module API class Discussions < Grape::API include PaginationParams helpers ::API::Helpers::NotesHelpers + helpers ::RendersNotes before { authenticate! } @@ -27,12 +28,7 @@ module API 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 @@ -226,13 +222,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 diff --git a/spec/requests/api/discussions_spec.rb b/spec/requests/api/discussions_spec.rb index ca1ffe3c524..daf75eb82bb 100644 --- a/spec/requests/api/discussions_spec.rb +++ b/spec/requests/api/discussions_spec.rb @@ -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) } |