Commit 0d3e0ad8 authored by Alex Kalderimis's avatar Alex Kalderimis

Merge branch 'fix-perf-regression-for-graphql-board-issues' into 'master'

Fix board issues graphql query performance regression

See merge request gitlab-org/gitlab!71164
parents 50b832bb 5c62b8c8
...@@ -18,11 +18,8 @@ module Resolvers ...@@ -18,11 +18,8 @@ module Resolvers
filter_params = filters.merge(board_id: list.board.id, id: list.id) filter_params = filters.merge(board_id: list.board.id, id: list.id)
service = ::Boards::Issues::ListService.new(list.board.resource_parent, context[:current_user], filter_params) service = ::Boards::Issues::ListService.new(list.board.resource_parent, context[:current_user], filter_params)
pagination_connections = Gitlab::Graphql::Pagination::Keyset::Connection.new(service.execute)
::Boards::Issues::ListService.initialize_relative_positions(list.board, current_user, pagination_connections.items) service.execute
pagination_connections
end end
# https://gitlab.com/gitlab-org/gitlab/-/issues/235681 # https://gitlab.com/gitlab-org/gitlab/-/issues/235681
......
...@@ -21,6 +21,7 @@ module Types ...@@ -21,6 +21,7 @@ module Types
@feature_flag = kwargs[:feature_flag] @feature_flag = kwargs[:feature_flag]
kwargs = check_feature_flag(kwargs) kwargs = check_feature_flag(kwargs)
@deprecation = gitlab_deprecation(kwargs) @deprecation = gitlab_deprecation(kwargs)
after_connection_extensions = kwargs.delete(:late_extensions) || []
super(**kwargs, &block) super(**kwargs, &block)
...@@ -28,6 +29,8 @@ module Types ...@@ -28,6 +29,8 @@ module Types
extension ::Gitlab::Graphql::CallsGitaly::FieldExtension if Gitlab.dev_or_test_env? extension ::Gitlab::Graphql::CallsGitaly::FieldExtension if Gitlab.dev_or_test_env?
extension ::Gitlab::Graphql::Present::FieldExtension extension ::Gitlab::Graphql::Present::FieldExtension
extension ::Gitlab::Graphql::Authorize::ConnectionFilterExtension extension ::Gitlab::Graphql::Authorize::ConnectionFilterExtension
after_connection_extensions.each { extension _1 } if after_connection_extensions.any?
end end
def may_call_gitaly? def may_call_gitaly?
......
...@@ -27,6 +27,7 @@ module Types ...@@ -27,6 +27,7 @@ module Types
field :issues, ::Types::IssueType.connection_type, null: true, field :issues, ::Types::IssueType.connection_type, null: true,
description: 'Board issues.', description: 'Board issues.',
late_extensions: [Gitlab::Graphql::Board::IssuesConnectionExtension],
resolver: ::Resolvers::BoardListIssuesResolver resolver: ::Resolvers::BoardListIssuesResolver
def issues_count def issues_count
......
...@@ -134,6 +134,6 @@ RSpec.describe Resolvers::BoardListIssuesResolver do ...@@ -134,6 +134,6 @@ RSpec.describe Resolvers::BoardListIssuesResolver do
end end
def resolve_board_list_issues(args) def resolve_board_list_issues(args)
resolve(described_class, obj: list, args: args, ctx: { current_user: user }).items resolve(described_class, obj: list, args: args, ctx: { current_user: user })
end end
end end
# frozen_string_literal: true
module Gitlab
module Graphql
module Board
class IssuesConnectionExtension < GraphQL::Schema::Field::ConnectionExtension
def after_resolve(value:, object:, context:, **rest)
::Boards::Issues::ListService
.initialize_relative_positions(object.list.board, context[:current_user], value.nodes)
value
end
end
end
end
end
...@@ -6,7 +6,7 @@ module Gitlab ...@@ -6,7 +6,7 @@ module Gitlab
extend ActiveSupport::Concern extend ActiveSupport::Concern
included do included do
delegate :to_a, :size, :include?, :empty?, to: :nodes delegate :to_a, :size, :map, :include?, :empty?, to: :nodes
end end
end end
end end
......
...@@ -31,12 +31,11 @@ RSpec.describe Resolvers::BoardListIssuesResolver do ...@@ -31,12 +31,11 @@ RSpec.describe Resolvers::BoardListIssuesResolver do
end.to raise_error(Gitlab::Graphql::Errors::ArgumentError) end.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
end end
it 'returns issues in the correct order with non-nil relative positions', :aggregate_failures do it 'returns the issues in the correct order' do
# by relative_position and then ID # by relative_position and then ID
result = resolve_board_list_issues result = resolve_board_list_issues
expect(result.map(&:id)).to eq [issue1.id, issue3.id, issue2.id, issue4.id] expect(result.map(&:id)).to eq [issue1.id, issue3.id, issue2.id, issue4.id]
expect(result.map(&:relative_position)).not_to include(nil)
end end
it 'finds only issues matching filters' do it 'finds only issues matching filters' do
...@@ -119,6 +118,6 @@ RSpec.describe Resolvers::BoardListIssuesResolver do ...@@ -119,6 +118,6 @@ RSpec.describe Resolvers::BoardListIssuesResolver do
end end
def resolve_board_list_issues(args: {}, current_user: user) def resolve_board_list_issues(args: {}, current_user: user)
resolve(described_class, obj: list, args: args, ctx: { current_user: current_user }).items resolve(described_class, obj: list, args: args, ctx: { current_user: current_user })
end end
end end
...@@ -154,6 +154,17 @@ RSpec.describe Types::BaseField do ...@@ -154,6 +154,17 @@ RSpec.describe Types::BaseField do
end end
end end
describe '#resolve' do
context "late_extensions is given" do
it 'registers the late extensions after the regular extensions' do
extension_class = Class.new(GraphQL::Schema::Field::ConnectionExtension)
field = described_class.new(name: 'test', type: GraphQL::Types::String.connection_type, null: true, late_extensions: [extension_class])
expect(field.extensions.last.class).to be(extension_class)
end
end
end
describe '#description' do describe '#description' do
context 'feature flag given' do context 'feature flag given' do
let(:field) { described_class.new(name: 'test', type: GraphQL::Types::String, feature_flag: flag, null: false, description: 'Test description.') } let(:field) { described_class.new(name: 'test', type: GraphQL::Types::String, feature_flag: flag, null: false, description: 'Test description.') }
......
...@@ -10,4 +10,12 @@ RSpec.describe GitlabSchema.types['BoardList'] do ...@@ -10,4 +10,12 @@ RSpec.describe GitlabSchema.types['BoardList'] do
expect(described_class).to include_graphql_fields(*expected_fields) expect(described_class).to include_graphql_fields(*expected_fields)
end end
describe 'issues field' do
subject { described_class.fields['issues'] }
it 'has a correct extension' do
is_expected.to have_graphql_extension(Gitlab::Graphql::Board::IssuesConnectionExtension)
end
end
end end
...@@ -30,7 +30,7 @@ RSpec.describe 'get board lists' do ...@@ -30,7 +30,7 @@ RSpec.describe 'get board lists' do
nodes { nodes {
lists { lists {
nodes { nodes {
issues(filters: {labelName: "#{label2.title}"}) { issues(filters: {labelName: "#{label2.title}"}, first: 3) {
count count
nodes { nodes {
#{all_graphql_fields_for('issues'.classify)} #{all_graphql_fields_for('issues'.classify)}
...@@ -44,6 +44,10 @@ RSpec.describe 'get board lists' do ...@@ -44,6 +44,10 @@ RSpec.describe 'get board lists' do
) )
end end
def issue_id
issues_data.map { |i| i['id'] }
end
def issue_titles def issue_titles
issues_data.map { |i| i['title'] } issues_data.map { |i| i['title'] }
end end
...@@ -60,6 +64,7 @@ RSpec.describe 'get board lists' do ...@@ -60,6 +64,7 @@ RSpec.describe 'get board lists' do
let!(:issue3) { create(:issue, project: issue_project, labels: [label, label2], relative_position: nil) } let!(:issue3) { create(:issue, project: issue_project, labels: [label, label2], relative_position: nil) }
let!(:issue4) { create(:issue, project: issue_project, labels: [label], relative_position: 9) } let!(:issue4) { create(:issue, project: issue_project, labels: [label], relative_position: 9) }
let!(:issue5) { create(:issue, project: issue_project, labels: [label2], relative_position: 432) } let!(:issue5) { create(:issue, project: issue_project, labels: [label2], relative_position: 432) }
let!(:issue6) { create(:issue, project: issue_project, labels: [label, label2], relative_position: nil) }
context 'when the user does not have access to the board' do context 'when the user does not have access to the board' do
it 'returns nil' do it 'returns nil' do
...@@ -72,14 +77,19 @@ RSpec.describe 'get board lists' do ...@@ -72,14 +77,19 @@ RSpec.describe 'get board lists' do
context 'when user can read the board' do context 'when user can read the board' do
before do before do
board_parent.add_reporter(user) board_parent.add_reporter(user)
post_graphql(query("id: \"#{global_id_of(label_list)}\""), current_user: user)
end end
it 'can access the issues', :aggregate_failures do it 'can access the issues', :aggregate_failures do
post_graphql(query("id: \"#{global_id_of(label_list)}\""), current_user: user) # ties for relative positions are broken by id in ascending order by default
expect(issue_titles).to eq([issue2.title, issue1.title, issue3.title]) expect(issue_titles).to eq([issue2.title, issue1.title, issue3.title])
expect(issue_relative_positions).not_to include(nil) expect(issue_relative_positions).not_to include(nil)
end end
it 'does not set the relative positions of the issues not being returned', :aggregate_failures do
expect(issue_id).not_to include(issue6.id)
expect(issue3.relative_position).to be_nil
end
end end
end end
......
# frozen_string_literal: true # frozen_string_literal: true
RSpec.shared_examples 'a connection with collection methods' do RSpec.shared_examples 'a connection with collection methods' do
%i[to_a size include? empty?].each do |method_name| %i[to_a size map include? empty?].each do |method_name|
it "responds to #{method_name}" do it "responds to #{method_name}" do
expect(connection).to respond_to(method_name) expect(connection).to respond_to(method_name)
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