From cd15b91b15598ee72dd4972a602306dd09152ad5 Mon Sep 17 00:00:00 2001 From: Adam Hegyi <ahegyi@gitlab.com> Date: Tue, 7 Feb 2023 12:58:38 +0100 Subject: [PATCH] Preload max access level when loading group labels Preload max access level when loading group labels to improve LabelsFinder performance. The change is behind a feature flag: preload_max_access_levels_for_labels_finder --- .../concerns/finder_with_group_hierarchy.rb | 6 ++ ...ad_max_access_levels_for_labels_finder.yml | 8 ++ .../finder_with_group_hierarchy_spec.rb | 98 ++++++++++++++++++- 3 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 config/feature_flags/development/preload_max_access_levels_for_labels_finder.yml diff --git a/app/finders/concerns/finder_with_group_hierarchy.rb b/app/finders/concerns/finder_with_group_hierarchy.rb index 86ccac19b63a0..4ced544ba2cf4 100644 --- a/app/finders/concerns/finder_with_group_hierarchy.rb +++ b/app/finders/concerns/finder_with_group_hierarchy.rb @@ -27,6 +27,12 @@ def group_ids_for(group) # we can preset root group for all of them to optimize permission checks Group.preset_root_ancestor_for(groups) + # Preloading the max access level for the given groups to avoid N+1 queries + # during the access check. + if !skip_authorization && current_user && Feature.enabled?(:preload_max_access_levels_for_labels_finder, group) + Preloaders::UserMaxAccessLevelInGroupsPreloader.new(groups, current_user).execute + end + groups_user_can_read_items(groups).map(&:id) end end diff --git a/config/feature_flags/development/preload_max_access_levels_for_labels_finder.yml b/config/feature_flags/development/preload_max_access_levels_for_labels_finder.yml new file mode 100644 index 0000000000000..456616384327f --- /dev/null +++ b/config/feature_flags/development/preload_max_access_levels_for_labels_finder.yml @@ -0,0 +1,8 @@ +--- +name: preload_max_access_levels_for_labels_finder +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111313 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/390664 +milestone: '15.9' +type: development +group: group::optimize +default_enabled: false diff --git a/spec/finders/concerns/finder_with_group_hierarchy_spec.rb b/spec/finders/concerns/finder_with_group_hierarchy_spec.rb index 8c2026a00a11a..c96e35372d6f8 100644 --- a/spec/finders/concerns/finder_with_group_hierarchy_spec.rb +++ b/spec/finders/concerns/finder_with_group_hierarchy_spec.rb @@ -40,7 +40,7 @@ def read_permission let_it_be(:private_group) { create(:group, :private) } let_it_be(:private_subgroup) { create(:group, :private, parent: private_group) } - let(:user) { create(:user) } + let!(:user) { create(:user) } context 'when specifying group' do it 'returns only the group by default' do @@ -109,4 +109,100 @@ def read_permission expect(finder.execute(skip_authorization: true)).to match_array([private_group.id, private_subgroup.id]) end end + + context 'with N+1 query check' do + def run_query(group) + finder_class + .new(user, group: group, include_descendant_groups: true) + .execute + .to_a + + RequestStore.clear! + end + + it 'does not produce N+1 query', :request_store do + private_group.add_developer(user) + + run_query(private_subgroup) # warmup + control = ActiveRecord::QueryRecorder.new { run_query(private_subgroup) } + + expect { run_query(private_group) }.not_to exceed_query_limit(control) + end + end + + context 'when preload_max_access_levels_for_labels_finder is disabled' do + # All test cases were copied from above, these will be removed once the FF is removed. + + before do + stub_feature_flags(preload_max_access_levels_for_labels_finder: false) + end + + context 'when specifying group' do + it 'returns only the group by default' do + finder = finder_class.new(user, group: group) + + expect(finder.execute).to match_array([group.id]) + end + end + + context 'when specifying group_id' do + it 'returns only the group by default' do + finder = finder_class.new(user, group_id: group.id) + + expect(finder.execute).to match_array([group.id]) + end + end + + context 'when including items from group ancestors' do + before do + private_subgroup.add_developer(user) + end + + it 'returns group and its ancestors' do + private_group.add_developer(user) + + finder = finder_class.new(user, group: private_subgroup, include_ancestor_groups: true) + + expect(finder.execute).to match_array([private_group.id, private_subgroup.id]) + end + + it 'ignores groups which user can not read' do + finder = finder_class.new(user, group: private_subgroup, include_ancestor_groups: true) + + expect(finder.execute).to match_array([private_subgroup.id]) + end + + it 'returns them all when skip_authorization is true' do + finder = finder_class.new(user, group: private_subgroup, include_ancestor_groups: true) + + expect(finder.execute(skip_authorization: true)).to match_array([private_group.id, private_subgroup.id]) + end + end + + context 'when including items from group descendants' do + before do + private_subgroup.add_developer(user) + end + + it 'returns items from group and its descendants' do + private_group.add_developer(user) + + finder = finder_class.new(user, group: private_group, include_descendant_groups: true) + + expect(finder.execute).to match_array([private_group.id, private_subgroup.id]) + end + + it 'ignores items from groups which user can not read' do + finder = finder_class.new(user, group: private_group, include_descendant_groups: true) + + expect(finder.execute).to match_array([private_subgroup.id]) + end + + it 'returns them all when skip_authorization is true' do + finder = finder_class.new(user, group: private_group, include_descendant_groups: true) + + expect(finder.execute(skip_authorization: true)).to match_array([private_group.id, private_subgroup.id]) + end + end + end end -- GitLab