Commit 111a1245 authored by Furkan Ayhan's avatar Furkan Ayhan Committed by Kamil Trzciński

Eliminate cross-db-join in RunnersFinder

parent f13747c5
...@@ -53,9 +53,13 @@ module Ci ...@@ -53,9 +53,13 @@ module Ci
when :direct when :direct
Ci::Runner.belonging_to_group(@group.id) Ci::Runner.belonging_to_group(@group.id)
when :descendants, nil when :descendants, nil
# Getting all runners from the group itself and all its descendant groups/projects if ::Feature.enabled?(:ci_find_runners_by_ci_mirrors, @group, default_enabled: :yaml)
descendant_projects = Project.for_group_and_its_subgroups(@group) Ci::Runner.belonging_to_group_or_project_descendants(@group.id)
Ci::Runner.belonging_to_group_or_project(@group.self_and_descendants, descendant_projects) else
# Getting all runners from the group itself and all its descendant groups/projects
descendant_projects = Project.for_group_and_its_subgroups(@group)
Ci::Runner.legacy_belonging_to_group_or_project(@group.self_and_descendants, descendant_projects)
end
else else
raise ArgumentError, 'Invalid membership filter' raise ArgumentError, 'Invalid membership filter'
end end
......
...@@ -10,6 +10,8 @@ module Ci ...@@ -10,6 +10,8 @@ module Ci
where('traversal_ids @> ARRAY[?]::int[]', id) where('traversal_ids @> ARRAY[?]::int[]', id)
end end
scope :by_namespace_id, -> (namespace_id) { where(namespace_id: namespace_id) }
class << self class << self
def sync!(event) def sync!(event)
namespace = event.namespace namespace = event.namespace
......
...@@ -6,6 +6,9 @@ module Ci ...@@ -6,6 +6,9 @@ module Ci
class ProjectMirror < ApplicationRecord class ProjectMirror < ApplicationRecord
belongs_to :project belongs_to :project
scope :by_namespace_id, -> (namespace_id) { where(namespace_id: namespace_id) }
scope :by_project_id, -> (project_id) { where(project_id: project_id) }
class << self class << self
def sync!(event) def sync!(event)
upsert({ project_id: event.project_id, namespace_id: event.project.namespace_id }, upsert({ project_id: event.project_id, namespace_id: event.project.namespace_id },
......
...@@ -95,7 +95,26 @@ module Ci ...@@ -95,7 +95,26 @@ module Ci
joins(:runner_projects).where(ci_runner_projects: { project_id: project_id }) joins(:runner_projects).where(ci_runner_projects: { project_id: project_id })
} }
scope :belonging_to_group, -> (group_id, include_ancestors: false) { scope :belonging_to_group, -> (group_id) {
joins(:runner_namespaces)
.where(ci_runner_namespaces: { namespace_id: group_id })
}
scope :belonging_to_group_or_project_descendants, -> (group_id) {
group_ids = Ci::NamespaceMirror.contains_namespace(group_id).select(:namespace_id)
project_ids = Ci::ProjectMirror.by_namespace_id(group_ids).select(:project_id)
group_runners = joins(:runner_namespaces).where(ci_runner_namespaces: { namespace_id: group_ids })
project_runners = joins(:runner_projects).where(ci_runner_projects: { project_id: project_ids })
union_sql = ::Gitlab::SQL::Union.new([group_runners, project_runners]).to_sql
from("(#{union_sql}) #{table_name}")
}
# deprecated
# split this into: belonging_to_group & belonging_to_group_and_ancestors
scope :legacy_belonging_to_group, -> (group_id, include_ancestors: false) {
groups = ::Group.where(id: group_id) groups = ::Group.where(id: group_id)
groups = groups.self_and_ancestors if include_ancestors groups = groups.self_and_ancestors if include_ancestors
...@@ -104,7 +123,8 @@ module Ci ...@@ -104,7 +123,8 @@ module Ci
.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/336433') .allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/336433')
} }
scope :belonging_to_group_or_project, -> (group_id, project_id) { # deprecated
scope :legacy_belonging_to_group_or_project, -> (group_id, project_id) {
groups = ::Group.where(id: group_id) groups = ::Group.where(id: group_id)
group_runners = joins(:runner_namespaces).where(ci_runner_namespaces: { namespace_id: groups }) group_runners = joins(:runner_namespaces).where(ci_runner_namespaces: { namespace_id: groups })
...@@ -116,6 +136,7 @@ module Ci ...@@ -116,6 +136,7 @@ module Ci
.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/336433') .allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/336433')
} }
# deprecated
scope :belonging_to_parent_group_of_project, -> (project_id) { scope :belonging_to_parent_group_of_project, -> (project_id) {
project_groups = ::Group.joins(:projects).where(projects: { id: project_id }) project_groups = ::Group.joins(:projects).where(projects: { id: project_id })
...@@ -124,6 +145,7 @@ module Ci ...@@ -124,6 +145,7 @@ module Ci
.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/336433') .allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/336433')
} }
# deprecated
scope :owned_or_instance_wide, -> (project_id) do scope :owned_or_instance_wide, -> (project_id) do
from_union( from_union(
[ [
......
---
name: ci_find_runners_by_ci_mirrors
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74900
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/347226
milestone: '14.7'
type: development
group: group::runner
default_enabled: false
...@@ -62,7 +62,7 @@ module Analytics ...@@ -62,7 +62,7 @@ module Analytics
def runner_configured def runner_configured
::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/337541') do ::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/337541') do
Ci::Runner.active.belonging_to_group_or_project(snapshot_groups, snapshot_project_ids).exists? Ci::Runner.active.legacy_belonging_to_group_or_project(snapshot_groups, snapshot_project_ids).exists?
end end
end end
......
...@@ -229,7 +229,7 @@ module API ...@@ -229,7 +229,7 @@ module API
use :pagination use :pagination
end end
get ':id/runners' do get ':id/runners' do
runners = ::Ci::Runner.belonging_to_group(user_group.id, include_ancestors: true) runners = ::Ci::Runner.legacy_belonging_to_group(user_group.id, include_ancestors: true)
runners = apply_filter(runners, params) runners = apply_filter(runners, params)
present paginate(runners), with: Entities::Ci::Runner present paginate(runners), with: Entities::Ci::Runner
......
...@@ -11,6 +11,14 @@ FactoryBot.define do ...@@ -11,6 +11,14 @@ FactoryBot.define do
owner { association(:user, strategy: :build, namespace: instance, username: path) } owner { association(:user, strategy: :build, namespace: instance, username: path) }
after(:create) do |namespace, evaluator|
# simulating ::Namespaces::ProcessSyncEventsWorker because most tests don't run Sidekiq inline
# Note: we need to get refreshed `traversal_ids` it is updated via SQL query
# in `Namespaces::Traversal::Linear#sync_traversal_ids` (see the NOTE in that method).
# We cannot use `.reload` because it cleans other on-the-fly attributes.
namespace.create_ci_namespace_mirror!(traversal_ids: Namespace.find(namespace.id).traversal_ids) unless namespace.ci_namespace_mirror
end
trait :with_aggregation_schedule do trait :with_aggregation_schedule do
after(:create) do |namespace| after(:create) do |namespace|
create(:namespace_aggregation_schedules, namespace: namespace) create(:namespace_aggregation_schedules, namespace: namespace)
......
...@@ -101,6 +101,9 @@ FactoryBot.define do ...@@ -101,6 +101,9 @@ FactoryBot.define do
import_state.last_error = evaluator.import_last_error import_state.last_error = evaluator.import_last_error
import_state.save! import_state.save!
end end
# simulating ::Projects::ProcessSyncEventsWorker because most tests don't run Sidekiq inline
project.create_ci_project_mirror!(namespace_id: project.namespace_id) unless project.ci_project_mirror
end end
trait :public do trait :public do
......
...@@ -206,7 +206,7 @@ RSpec.describe Ci::RunnersFinder do ...@@ -206,7 +206,7 @@ RSpec.describe Ci::RunnersFinder do
sub_group_4.runners << runner_sub_group_4 sub_group_4.runners << runner_sub_group_4
end end
describe '#execute' do shared_examples '#execute' do
subject { described_class.new(current_user: user, params: params).execute } subject { described_class.new(current_user: user, params: params).execute }
shared_examples 'membership equal to :descendants' do shared_examples 'membership equal to :descendants' do
...@@ -349,6 +349,16 @@ RSpec.describe Ci::RunnersFinder do ...@@ -349,6 +349,16 @@ RSpec.describe Ci::RunnersFinder do
end end
end end
it_behaves_like '#execute'
context 'when the FF ci_find_runners_by_ci_mirrors is disabled' do
before do
stub_feature_flags(ci_find_runners_by_ci_mirrors: false)
end
it_behaves_like '#execute'
end
describe '#sort_key' do describe '#sort_key' do
subject { described_class.new(current_user: user, params: params.merge(group: group)).sort_key } subject { described_class.new(current_user: user, params: params.merge(group: group)).sort_key }
......
...@@ -8,50 +8,77 @@ RSpec.describe Ci::NamespaceMirror do ...@@ -8,50 +8,77 @@ RSpec.describe Ci::NamespaceMirror do
let!(:group3) { create(:group, parent: group2) } let!(:group3) { create(:group, parent: group2) }
let!(:group4) { create(:group, parent: group3) } let!(:group4) { create(:group, parent: group3) }
describe '.sync!' do before do
let!(:event) { namespace.sync_events.create! } # refreshing ci mirrors according to the parent tree above
Namespaces::SyncEvent.find_each { |event| Ci::NamespaceMirror.sync!(event) }
# checking initial situation. we need to reload to reflect the changes of event sync
expect(group1.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group1.id])
expect(group2.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group1.id, group2.id])
expect(group3.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group1.id, group2.id, group3.id])
expect(group4.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group1.id, group2.id, group3.id, group4.id])
end
context 'scopes' do
describe '.contains_namespace' do
let_it_be(:another_group) { create(:group) }
subject(:result) { described_class.contains_namespace(group2.id) }
it 'returns groups having group2.id in traversal_ids' do
expect(result.pluck(:namespace_id)).to contain_exactly(group2.id, group3.id, group4.id)
end
end
describe '.by_namespace_id' do
subject(:result) { described_class.by_namespace_id(group2.id) }
subject(:sync) { described_class.sync!(event.reload) } it 'returns namesapce mirrors of namespace id' do
expect(result).to contain_exactly(group2.ci_namespace_mirror)
end
end
end
context 'when namespace hierarchy does not exist in the first place' do describe '.sync!' do
subject(:sync) { described_class.sync!(Namespaces::SyncEvent.last) }
context 'when namespace mirror does not exist in the first place' do
let(:namespace) { group3 } let(:namespace) { group3 }
it 'creates the hierarchy' do before do
expect { sync }.to change { described_class.count }.from(0).to(1) namespace.ci_namespace_mirror.destroy!
namespace.sync_events.create!
end
it 'creates the mirror' do
expect { sync }.to change { described_class.count }.from(3).to(4)
expect(namespace.ci_namespace_mirror).to have_attributes(traversal_ids: [group1.id, group2.id, group3.id]) expect(namespace.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group1.id, group2.id, group3.id])
end end
end end
context 'when namespace hierarchy does already exist' do context 'when namespace mirror does already exist' do
let(:namespace) { group3 } let(:namespace) { group3 }
before do before do
described_class.create!(namespace: namespace, traversal_ids: [namespace.id]) namespace.sync_events.create!
end end
it 'updates the hierarchy' do it 'updates the mirror' do
expect { sync }.not_to change { described_class.count } expect { sync }.not_to change { described_class.count }
expect(namespace.ci_namespace_mirror).to have_attributes(traversal_ids: [group1.id, group2.id, group3.id]) expect(namespace.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group1.id, group2.id, group3.id])
end end
end end
# I did not extract this context to a `shared_context` because the behavior will change shared_context 'changing the middle namespace' do
# after implementing the TODO in `Ci::NamespaceMirror.sync!`
context 'changing the middle namespace' do
let(:namespace) { group2 } let(:namespace) { group2 }
before do before do
described_class.create!(namespace_id: group1.id, traversal_ids: [group1.id]) group2.update!(parent: nil) # creates a sync event
described_class.create!(namespace_id: group2.id, traversal_ids: [group1.id, group2.id])
described_class.create!(namespace_id: group3.id, traversal_ids: [group1.id, group2.id, group3.id])
described_class.create!(namespace_id: group4.id, traversal_ids: [group1.id, group2.id, group3.id, group4.id])
group2.update!(parent: nil)
end end
it 'updates hierarchies for the base but wait for events for the children' do it 'updates traversal_ids for the base and descendants' do
expect { sync }.not_to change { described_class.count } expect { sync }.not_to change { described_class.count }
expect(group1.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group1.id]) expect(group1.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group1.id])
...@@ -61,6 +88,8 @@ RSpec.describe Ci::NamespaceMirror do ...@@ -61,6 +88,8 @@ RSpec.describe Ci::NamespaceMirror do
end end
end end
it_behaves_like 'changing the middle namespace'
context 'when the FFs sync_traversal_ids, use_traversal_ids and use_traversal_ids_for_ancestors are disabled' do context 'when the FFs sync_traversal_ids, use_traversal_ids and use_traversal_ids_for_ancestors are disabled' do
before do before do
stub_feature_flags(sync_traversal_ids: false, stub_feature_flags(sync_traversal_ids: false,
...@@ -68,27 +97,7 @@ RSpec.describe Ci::NamespaceMirror do ...@@ -68,27 +97,7 @@ RSpec.describe Ci::NamespaceMirror do
use_traversal_ids_for_ancestors: false) use_traversal_ids_for_ancestors: false)
end end
context 'changing the middle namespace' do it_behaves_like 'changing the middle namespace'
let(:namespace) { group2 }
before do
described_class.create!(namespace_id: group1.id, traversal_ids: [group1.id])
described_class.create!(namespace_id: group2.id, traversal_ids: [group1.id, group2.id])
described_class.create!(namespace_id: group3.id, traversal_ids: [group1.id, group2.id, group3.id])
described_class.create!(namespace_id: group4.id, traversal_ids: [group1.id, group2.id, group3.id, group4.id])
group2.update!(parent: nil)
end
it 'updates hierarchies for the base and descendants' do
expect { sync }.not_to change { described_class.count }
expect(group1.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group1.id])
expect(group2.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group2.id])
expect(group3.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group2.id, group3.id])
expect(group4.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group2.id, group3.id, group4.id])
end
end
end end
end end
end end
...@@ -8,12 +8,36 @@ RSpec.describe Ci::ProjectMirror do ...@@ -8,12 +8,36 @@ RSpec.describe Ci::ProjectMirror do
let!(:project) { create(:project, namespace: group2) } let!(:project) { create(:project, namespace: group2) }
context 'scopes' do
let_it_be(:another_project) { create(:project, namespace: group1) }
describe '.by_project_id' do
subject(:result) { described_class.by_project_id(project.id) }
it 'returns project mirrors of project' do
expect(result.pluck(:project_id)).to contain_exactly(project.id)
end
end
describe '.by_namespace_id' do
subject(:result) { described_class.by_namespace_id(group2.id) }
it 'returns project mirrors of namespace id' do
expect(result).to contain_exactly(project.ci_project_mirror)
end
end
end
describe '.sync!' do describe '.sync!' do
let!(:event) { Projects::SyncEvent.create!(project: project) } let!(:event) { Projects::SyncEvent.create!(project: project) }
subject(:sync) { described_class.sync!(event.reload) } subject(:sync) { described_class.sync!(event) }
context 'when project mirror does not exist in the first place' do
before do
project.ci_project_mirror.destroy!
end
context 'when project hierarchy does not exist in the first place' do
it 'creates a ci_projects record' do it 'creates a ci_projects record' do
expect { sync }.to change { described_class.count }.from(0).to(1) expect { sync }.to change { described_class.count }.from(0).to(1)
...@@ -21,11 +45,7 @@ RSpec.describe Ci::ProjectMirror do ...@@ -21,11 +45,7 @@ RSpec.describe Ci::ProjectMirror do
end end
end end
context 'when project hierarchy does already exist' do context 'when project mirror does already exist' do
before do
described_class.create!(project_id: project.id, namespace_id: group1.id)
end
it 'updates the related ci_projects record' do it 'updates the related ci_projects record' do
expect { sync }.not_to change { described_class.count } expect { sync }.not_to change { described_class.count }
......
...@@ -1358,7 +1358,7 @@ RSpec.describe Ci::Runner do ...@@ -1358,7 +1358,7 @@ RSpec.describe Ci::Runner do
it { is_expected.to eq(contacted_at_stored) } it { is_expected.to eq(contacted_at_stored) }
end end
describe '.belonging_to_group' do describe '.legacy_belonging_to_group' do
shared_examples 'returns group runners' do shared_examples 'returns group runners' do
it 'returns the specific group runner' do it 'returns the specific group runner' do
group = create(:group) group = create(:group)
...@@ -1366,7 +1366,7 @@ RSpec.describe Ci::Runner do ...@@ -1366,7 +1366,7 @@ RSpec.describe Ci::Runner do
unrelated_group = create(:group) unrelated_group = create(:group)
create(:ci_runner, :group, groups: [unrelated_group]) create(:ci_runner, :group, groups: [unrelated_group])
expect(described_class.belonging_to_group(group.id)).to contain_exactly(runner) expect(described_class.legacy_belonging_to_group(group.id)).to contain_exactly(runner)
end end
context 'runner belonging to parent group' do context 'runner belonging to parent group' do
...@@ -1376,13 +1376,13 @@ RSpec.describe Ci::Runner do ...@@ -1376,13 +1376,13 @@ RSpec.describe Ci::Runner do
context 'when include_parent option is passed' do context 'when include_parent option is passed' do
it 'returns the group runner from the parent group' do it 'returns the group runner from the parent group' do
expect(described_class.belonging_to_group(group.id, include_ancestors: true)).to contain_exactly(parent_runner) expect(described_class.legacy_belonging_to_group(group.id, include_ancestors: true)).to contain_exactly(parent_runner)
end end
end end
context 'when include_parent option is not passed' do context 'when include_parent option is not passed' do
it 'does not return the group runner from the parent group' do it 'does not return the group runner from the parent group' do
expect(described_class.belonging_to_group(group.id)).to be_empty expect(described_class.legacy_belonging_to_group(group.id)).to be_empty
end end
end end
end end
...@@ -1398,4 +1398,38 @@ RSpec.describe Ci::Runner do ...@@ -1398,4 +1398,38 @@ RSpec.describe Ci::Runner do
it_behaves_like 'returns group runners' it_behaves_like 'returns group runners'
end end
end end
describe '.belonging_to_group' do
it 'returns the specific group runner' do
group = create(:group)
runner = create(:ci_runner, :group, groups: [group])
unrelated_group = create(:group)
create(:ci_runner, :group, groups: [unrelated_group])
expect(described_class.belonging_to_group(group.id)).to contain_exactly(runner)
end
end
describe '.belonging_to_group_or_project_descendants' do
it 'returns the specific group runners' do
group1 = create(:group)
group2 = create(:group, parent: group1)
group3 = create(:group)
project1 = create(:project, namespace: group1)
project2 = create(:project, namespace: group2)
project3 = create(:project, namespace: group3)
runner1 = create(:ci_runner, :group, groups: [group1])
runner2 = create(:ci_runner, :group, groups: [group2])
_runner3 = create(:ci_runner, :group, groups: [group3])
runner4 = create(:ci_runner, :project, projects: [project1])
runner5 = create(:ci_runner, :project, projects: [project2])
_runner6 = create(:ci_runner, :project, projects: [project3])
expect(described_class.belonging_to_group_or_project_descendants(group1.id)).to contain_exactly(
runner1, runner2, runner4, runner5
)
end
end
end end
...@@ -28,10 +28,10 @@ RSpec.describe Ci::ProcessSyncEventsService do ...@@ -28,10 +28,10 @@ RSpec.describe Ci::ProcessSyncEventsService do
it 'consumes events' do it 'consumes events' do
expect { execute }.to change(Projects::SyncEvent, :count).from(2).to(0) expect { execute }.to change(Projects::SyncEvent, :count).from(2).to(0)
expect(project1.ci_project_mirror).to have_attributes( expect(project1.reload.ci_project_mirror).to have_attributes(
namespace_id: parent_group_1.id namespace_id: parent_group_1.id
) )
expect(project2.ci_project_mirror).to have_attributes( expect(project2.reload.ci_project_mirror).to have_attributes(
namespace_id: parent_group_2.id namespace_id: parent_group_2.id
) )
end end
...@@ -88,10 +88,10 @@ RSpec.describe Ci::ProcessSyncEventsService do ...@@ -88,10 +88,10 @@ RSpec.describe Ci::ProcessSyncEventsService do
it 'consumes events' do it 'consumes events' do
expect { execute }.to change(Namespaces::SyncEvent, :count).from(2).to(0) expect { execute }.to change(Namespaces::SyncEvent, :count).from(2).to(0)
expect(group.ci_namespace_mirror).to have_attributes( expect(group.reload.ci_namespace_mirror).to have_attributes(
traversal_ids: [parent_group_1.id, parent_group_2.id, group.id] traversal_ids: [parent_group_1.id, parent_group_2.id, group.id]
) )
expect(parent_group_2.ci_namespace_mirror).to have_attributes( expect(parent_group_2.reload.ci_namespace_mirror).to have_attributes(
traversal_ids: [parent_group_1.id, parent_group_2.id] traversal_ids: [parent_group_1.id, parent_group_2.id]
) )
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