Commit 123ecd49 authored by Kamil Trzciński's avatar Kamil Trzciński

Optimise ci_namespace_mirrors_for_group_members to search prefix

Currently the `ci_namespace_mirrors_for_group_members` would fetch all
IDs of all groups that user is member of. However, in most of cases
user is owner/maintainer of many subgroups. To optimise heavily
for this usage pattern find the top-most prefixes (shortest)
to avoid searching having to include IDs of childrens.

Changelog: changed
parent 2a4b6349
...@@ -11,6 +11,8 @@ module Ci ...@@ -11,6 +11,8 @@ module Ci
end end
scope :contains_any_of_namespaces, -> (ids) do scope :contains_any_of_namespaces, -> (ids) do
return none if ids.empty?
where('traversal_ids && ARRAY[?]::int[]', ids) where('traversal_ids && ARRAY[?]::int[]', ids)
end end
......
...@@ -49,6 +49,33 @@ module Namespaces ...@@ -49,6 +49,33 @@ module Namespaces
before_commit :sync_traversal_ids, on: [:create], if: -> { sync_traversal_ids? } before_commit :sync_traversal_ids, on: [:create], if: -> { sync_traversal_ids? }
end end
class_methods do
# This method looks into a list of namespaces trying to optimise a returned traversal_ids
# into a list of shortest prefixes, due to fact that the shortest prefixes include all childrens.
# Example:
# INPUT: [[4909902], [4909902,51065789], [4909902,51065793], [7135830], [15599674, 1], [15599674, 1, 3], [15599674, 2]]
# RESULT: [[4909902], [7135830], [15599674, 1], [15599674, 2]]
def shortest_traversal_ids_prefixes
raise ArgumentError, 'Feature not supported since the `:use_traversal_ids` is disabled' unless use_traversal_ids?
prefixes = []
# The array needs to be sorted (O(nlogn)) to ensure shortest elements are always first
# This allows to do O(n) search of shortest prefixes
all_traversal_ids = all.order('namespaces.traversal_ids').pluck('namespaces.traversal_ids')
last_prefix = [nil]
all_traversal_ids.each do |traversal_ids|
next if last_prefix == traversal_ids[0..(last_prefix.count - 1)]
last_prefix = traversal_ids
prefixes << traversal_ids
end
prefixes
end
end
def sync_traversal_ids? def sync_traversal_ids?
Feature.enabled?(:sync_traversal_ids, root_ancestor, default_enabled: :yaml) Feature.enabled?(:sync_traversal_ids, root_ancestor, default_enabled: :yaml)
end end
......
...@@ -2244,9 +2244,23 @@ class User < ApplicationRecord ...@@ -2244,9 +2244,23 @@ class User < ApplicationRecord
end end
def ci_namespace_mirrors_for_group_members(level) def ci_namespace_mirrors_for_group_members(level)
Ci::NamespaceMirror.contains_any_of_namespaces( search_members = group_members.where('access_level >= ?', level)
group_members.where('access_level >= ?', level).pluck(:source_id)
) # This reduces searched prefixes to only shortest ones
# to avoid querying descendants since they are already covered
# by ancestor namespaces. If the FF is not available fallback to
# inefficient search: https://gitlab.com/gitlab-org/gitlab/-/issues/336436
namespace_ids =
if Feature.enabled?(:use_traversal_ids, default_enabled: :yaml)
Group.joins(:all_group_members)
.merge(search_members)
.shortest_traversal_ids_prefixes
.map(&:last)
else
search_members.pluck(:source_id)
end
Ci::NamespaceMirror.contains_any_of_namespaces(namespace_ids)
end end
end end
......
...@@ -535,6 +535,10 @@ RSpec.describe Group do ...@@ -535,6 +535,10 @@ RSpec.describe Group do
describe '#ancestors_upto' do describe '#ancestors_upto' do
it { expect(group.ancestors_upto.to_sql).not_to include "WITH ORDINALITY" } it { expect(group.ancestors_upto.to_sql).not_to include "WITH ORDINALITY" }
end end
describe '.shortest_traversal_ids_prefixes' do
it { expect { described_class.shortest_traversal_ids_prefixes }.to raise_error /Feature not supported since the `:use_traversal_ids` is disabled/ }
end
end end
context 'linear' do context 'linear' do
...@@ -576,6 +580,90 @@ RSpec.describe Group do ...@@ -576,6 +580,90 @@ RSpec.describe Group do
it { expect(group.ancestors_upto.to_sql).to include "WITH ORDINALITY" } it { expect(group.ancestors_upto.to_sql).to include "WITH ORDINALITY" }
end end
describe '.shortest_traversal_ids_prefixes' do
subject { filter.shortest_traversal_ids_prefixes }
context 'for many top-level namespaces' do
let!(:top_level_groups) { create_list(:group, 4) }
context 'when querying all groups' do
let(:filter) { described_class.id_in(top_level_groups) }
it "returns all traversal_ids" do
is_expected.to contain_exactly(
*top_level_groups.map { |group| [group.id] }
)
end
end
context 'when querying selected groups' do
let(:filter) { described_class.id_in(top_level_groups.first) }
it "returns only a selected traversal_ids" do
is_expected.to contain_exactly([top_level_groups.first.id])
end
end
end
context 'for namespace hierarchy' do
let!(:group_a) { create(:group) }
let!(:group_a_sub_1) { create(:group, parent: group_a) }
let!(:group_a_sub_2) { create(:group, parent: group_a) }
let!(:group_b) { create(:group) }
let!(:group_b_sub_1) { create(:group, parent: group_b) }
let!(:group_c) { create(:group) }
context 'when querying all groups' do
let(:filter) { described_class.id_in([group_a, group_a_sub_1, group_a_sub_2, group_b, group_b_sub_1, group_c]) }
it 'returns only shortest prefixes of top-level groups' do
is_expected.to contain_exactly(
[group_a.id],
[group_b.id],
[group_c.id]
)
end
end
context 'when sub-group is reparented' do
let(:filter) { described_class.id_in([group_b_sub_1, group_c]) }
before do
group_b_sub_1.update!(parent: group_c)
end
it 'returns a proper shortest prefix of a new group' do
is_expected.to contain_exactly(
[group_c.id]
)
end
end
context 'when querying sub-groups' do
let(:filter) { described_class.id_in([group_a_sub_1, group_b_sub_1, group_c]) }
it 'returns sub-groups as they are shortest prefixes' do
is_expected.to contain_exactly(
[group_a.id, group_a_sub_1.id],
[group_b.id, group_b_sub_1.id],
[group_c.id]
)
end
end
context 'when querying group and sub-group of this group' do
let(:filter) { described_class.id_in([group_a, group_a_sub_1, group_c]) }
it 'returns parent groups as this contains all sub-groups' do
is_expected.to contain_exactly(
[group_a.id],
[group_c.id]
)
end
end
end
end
context 'when project namespace exists in the group' do context 'when project namespace exists in the group' do
let!(:project) { create(:project, group: group) } let!(:project) { create(:project, group: group) }
let!(:project_namespace) { project.project_namespace } let!(:project_namespace) { project.project_namespace }
......
...@@ -4251,8 +4251,17 @@ RSpec.describe User do ...@@ -4251,8 +4251,17 @@ RSpec.describe User do
end end
end end
describe '#ci_owned_runners' do
it_behaves_like '#ci_owned_runners' it_behaves_like '#ci_owned_runners'
context 'when FF use_traversal_ids is disabled fallbacks to inefficient implementation' do
before do
stub_feature_flags(use_traversal_ids: false)
end
it_behaves_like '#ci_owned_runners'
end
context 'when FF ci_owned_runners_cross_joins_fix is disabled' do context 'when FF ci_owned_runners_cross_joins_fix is disabled' do
before do before do
skip_if_multiple_databases_are_setup skip_if_multiple_databases_are_setup
...@@ -4262,6 +4271,7 @@ RSpec.describe User do ...@@ -4262,6 +4271,7 @@ RSpec.describe User do
it_behaves_like '#ci_owned_runners' it_behaves_like '#ci_owned_runners'
end end
end
describe '#projects_with_reporter_access_limited_to' do describe '#projects_with_reporter_access_limited_to' do
let(:project1) { create(:project) } let(:project1) { create(:project) }
......
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