Commit ba02cfd0 authored by Jan Provaznik's avatar Jan Provaznik

Remove CachingArrayResolver from epic issues

Because we won't have hard limit on number of issues associated with an
epic, we can't assume there won't be more than "max_page_size". This MR
removes usage of CachingArrayResolver from EpicIssues resolver, a
downside is there may be N+1 issue when user loads issues for multiple
epics with one request.

EE: true
Changelog: fixed
parent 4ab1ad04
...@@ -2,30 +2,16 @@ ...@@ -2,30 +2,16 @@
module Resolvers module Resolvers
class EpicIssuesResolver < BaseResolver class EpicIssuesResolver < BaseResolver
include CachingArrayResolver
type Types::EpicIssueType.connection_type, null: true type Types::EpicIssueType.connection_type, null: true
alias_method :epic, :object alias_method :epic, :object
def model_class # because epic issues are ordered by EpicIssue's relative position,
::Issue # we can not use batch loading to load epic issues for multiple epics at once
end # (assuming we don't load all issues for each epic but only a single page)
def resolve
def allowed?(issue) issues = Epic.related_issues(ids: epic.id, preload: { project: [:namespace, :project_feature] })
DeclarativePolicy.user_scope { issue.visible_to_user?(current_user) } offset_pagination(issues)
end
def query_input(**args)
epic.id
end
def query_for(id)
::Epic.related_issues(ids: id)
end
def preload
{ project: [:namespace, :project_feature] }
end end
end end
end end
...@@ -5,8 +5,8 @@ require 'spec_helper' ...@@ -5,8 +5,8 @@ require 'spec_helper'
RSpec.describe Resolvers::EpicIssuesResolver do RSpec.describe Resolvers::EpicIssuesResolver do
include GraphqlHelpers include GraphqlHelpers
let_it_be(:current_user) { create(:user) } let_it_be(:developer) { create(:user) }
let_it_be(:user) { create(:user) } let_it_be(:guest) { create(:user) }
let_it_be(:group) { create(:group, :public) } let_it_be(:group) { create(:group, :public) }
let_it_be(:project1) { create(:project, :public, group: group) } let_it_be(:project1) { create(:project, :public, group: group) }
let_it_be(:project2) { create(:project, :private, group: group) } let_it_be(:project2) { create(:project, :private, group: group) }
...@@ -16,13 +16,21 @@ RSpec.describe Resolvers::EpicIssuesResolver do ...@@ -16,13 +16,21 @@ RSpec.describe Resolvers::EpicIssuesResolver do
let_it_be(:issue2) { create(:issue, project: project1, confidential: true) } let_it_be(:issue2) { create(:issue, project: project1, confidential: true) }
let_it_be(:issue3) { create(:issue, project: project2) } let_it_be(:issue3) { create(:issue, project: project2) }
let_it_be(:issue4) { create(:issue, project: project2) } let_it_be(:issue4) { create(:issue, project: project2) }
let_it_be(:issue5) { create(:issue, project: project1) }
let_it_be(:epic_issue1) { create(:epic_issue, epic: epic1, issue: issue1, relative_position: 3) } let_it_be(:epic_issue1) { create(:epic_issue, epic: epic1, issue: issue1, relative_position: 3) }
let_it_be(:epic_issue2) { create(:epic_issue, epic: epic1, issue: issue2, relative_position: 2) } let_it_be(:epic_issue2) { create(:epic_issue, epic: epic1, issue: issue2, relative_position: 2) }
let_it_be(:epic_issue3) { create(:epic_issue, epic: epic2, issue: issue3, relative_position: 1) } let_it_be(:epic_issue3) { create(:epic_issue, epic: epic2, issue: issue3, relative_position: 1) }
let_it_be(:epic_issue4) { create(:epic_issue, epic: epic2, issue: issue4, relative_position: nil) } let_it_be(:epic_issue4) { create(:epic_issue, epic: epic2, issue: issue4, relative_position: nil) }
let_it_be(:epic_issue5) { create(:epic_issue, epic: epic1, issue: issue5, relative_position: nil) }
let(:schema) do
Class.new(GitlabSchema) do
default_max_page_size 100
end
end
before do before do
group.add_developer(current_user) group.add_developer(developer)
stub_licensed_features(epics: true) stub_licensed_features(epics: true)
end end
...@@ -31,28 +39,31 @@ RSpec.describe Resolvers::EpicIssuesResolver do ...@@ -31,28 +39,31 @@ RSpec.describe Resolvers::EpicIssuesResolver do
end end
describe '#resolve' do describe '#resolve' do
let(:epics) { [epic1, epic2] } using RSpec::Parameterized::TableSyntax
it 'finds all epic issues' do
result = epics.map { |epic| resolve_epic_issues(epic).to_a }
expect(result).to eq [[issue2, issue1], [issue3, issue4]] where(:epic, :user, :max_page_size, :has_next_page, :issues) do
ref(:epic1) | ref(:developer) | 100 | false | lazy { [issue2, issue1, issue5] }
ref(:epic1) | ref(:developer) | 2 | true | lazy { [issue2, issue1] }
ref(:epic1) | ref(:guest) | 100 | false | lazy { [issue1, issue5] }
ref(:epic2) | ref(:developer) | 100 | false | lazy { [issue3, issue4] }
ref(:epic2) | ref(:guest) | 100 | false | lazy { [] }
end end
it 'finds only epic issues that user can read' do with_them do
guest = create(:user) it 'returns only a page of issues user can read' do
result = resolve_epic_issues(epic, user, max_page_size)
result = expect(result.to_a).to eq issues
[ expect(result.has_next_page).to eq has_next_page
resolve_epic_issues(epic1, user: guest).to_a, end
resolve_epic_issues(epic2, user: guest).to_a
]
expect(result).to eq([[issue1], []])
end end
end end
def resolve_epic_issues(object, user: current_user) def resolve_epic_issues(object, user, max_page_size)
force(resolve(described_class, obj: object, ctx: { current_user: user })) resolver = described_class
opts = resolver.field_options
allow(resolver).to receive(:field_options).and_return(opts.merge(max_page_size: max_page_size))
force(resolve(resolver, obj: object, ctx: { current_user: user }))
end end
end end
...@@ -124,17 +124,24 @@ RSpec.describe 'Getting issues for an epic' do ...@@ -124,17 +124,24 @@ RSpec.describe 'Getting issues for an epic' do
expect(result[epic2.iid]).to match_array [issue2.to_global_id.to_s] expect(result[epic2.iid]).to match_array [issue2.to_global_id.to_s]
end end
it 'avoids N+1 queries' do it 'does limited number of N+1 queries' do
user_1 = create(:user, developer_projects: [project]) # extra queries:
user_2 = create(:user, developer_projects: [project]) # epic_issues - for each epic issues are loaded ordered by relatve_position
# issue_assignees - issue policy checks if user is between issue assignees
# when https://gitlab.com/gitlab-org/gitlab/-/issues/353375 is fixed, we can
# preload also issue assignees
extra_queries_count = 2
# warm-up query
post_graphql(epic_query(iid: epic.iid), current_user: user)
control_count = ActiveRecord::QueryRecorder.new(query_recorder_debug: true) do control_count = ActiveRecord::QueryRecorder.new(query_recorder_debug: true) do
post_graphql(epic_query(iid: epic.iid), current_user: user_1) post_graphql(epic_query(iid: epic.iid), current_user: user)
end end
expect do expect do
post_graphql(epic_query(params), current_user: user_2) post_graphql(epic_query(params), current_user: user)
end.not_to exceed_query_limit(control_count).ignoring(/FROM "namespaces"/) end.not_to exceed_query_limit(control_count).with_threshold(extra_queries_count)
expect(graphql_errors).to be_nil expect(graphql_errors).to be_nil
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