Commit 7f29e633 authored by Maxime Orefice's avatar Maxime Orefice Committed by Marius Bobin

Fix cross join query for Ci::Runner#projects

This commit fixes cross database queries for the runner.
We are leveraging disable_joins setting to make 2 SQL queries
instead of 1 in order to prevent cross database query.

Changelog: performance
parent 32f08f89
...@@ -85,7 +85,11 @@ class Admin::RunnersController < Admin::ApplicationController ...@@ -85,7 +85,11 @@ class Admin::RunnersController < Admin::ApplicationController
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def assign_builds_and_projects def assign_builds_and_projects
@builds = runner.builds.order('id DESC').preload_project_and_pipeline_project.first(30) @builds = runner
.builds
.order_id_desc
.preload_project_and_pipeline_project.first(30)
@projects = @projects =
if params[:search].present? if params[:search].present?
::Project.search(params[:search]) ::Project.search(params[:search])
...@@ -93,12 +97,10 @@ class Admin::RunnersController < Admin::ApplicationController ...@@ -93,12 +97,10 @@ class Admin::RunnersController < Admin::ApplicationController
Project.all Project.all
end end
::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338659') do runner_projects_ids = runner.runner_projects.pluck(:project_id)
@projects = @projects.where.not(id: runner.projects.select(:id)) if runner.projects.any? @projects = @projects.where.not(id: runner_projects_ids) if runner_projects_ids.any?
@projects = @projects.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338659') @projects = @projects.inc_routes
@projects = @projects.inc_routes @projects = @projects.page(params[:page]).per(30).without_count
@projects = @projects.page(params[:page]).per(30).without_count
end
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
end end
...@@ -67,7 +67,7 @@ module Ci ...@@ -67,7 +67,7 @@ module Ci
has_many :builds has_many :builds
has_many :runner_projects, inverse_of: :runner, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :runner_projects, inverse_of: :runner, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :projects, through: :runner_projects has_many :projects, through: :runner_projects, disable_joins: -> { ::Feature.enabled?(:ci_runner_projects_disable_joins, default_enabled: :yaml) }
has_many :runner_namespaces, inverse_of: :runner, autosave: true has_many :runner_namespaces, inverse_of: :runner, autosave: true
has_many :groups, through: :runner_namespaces, disable_joins: true has_many :groups, through: :runner_namespaces, disable_joins: true
...@@ -378,9 +378,7 @@ module Ci ...@@ -378,9 +378,7 @@ module Ci
end end
def only_for?(project) def only_for?(project)
::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338659') do !runner_projects.where.not(project_id: project.id).exists?
projects == [project]
end
end end
def short_sha def short_sha
......
---
name: ci_runner_projects_disable_joins
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/78372
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/350538
milestone: '14.8'
type: development
group: group::pipeline execution
default_enabled: false
# frozen_string_literal: true
class AddCiRunnerProjectIndexToRunnerIdAndProjectId < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
OLD_INDEX_NAME = 'index_ci_runner_projects_on_runner_id'
NEW_INDEX_NAME = 'index_ci_runner_projects_on_runner_id_and_project_id'
TABLE_NAME = :ci_runner_projects
def up
add_concurrent_index(TABLE_NAME, [:runner_id, :project_id], name: NEW_INDEX_NAME)
remove_concurrent_index_by_name(TABLE_NAME, OLD_INDEX_NAME)
end
def down
add_concurrent_index(TABLE_NAME, :runner_id, name: OLD_INDEX_NAME)
remove_concurrent_index_by_name(TABLE_NAME, NEW_INDEX_NAME)
end
end
966c42749e9d200f2b7295fdbb86e596c33510f0abbf431d40b09629e5e4a6aa
\ No newline at end of file
...@@ -25707,7 +25707,7 @@ CREATE UNIQUE INDEX index_ci_runner_namespaces_on_runner_id_and_namespace_id ON ...@@ -25707,7 +25707,7 @@ CREATE UNIQUE INDEX index_ci_runner_namespaces_on_runner_id_and_namespace_id ON
CREATE INDEX index_ci_runner_projects_on_project_id ON ci_runner_projects USING btree (project_id); CREATE INDEX index_ci_runner_projects_on_project_id ON ci_runner_projects USING btree (project_id);
CREATE INDEX index_ci_runner_projects_on_runner_id ON ci_runner_projects USING btree (runner_id); CREATE INDEX index_ci_runner_projects_on_runner_id_and_project_id ON ci_runner_projects USING btree (runner_id, project_id);
CREATE INDEX index_ci_runners_on_active ON ci_runners USING btree (active, id); CREATE INDEX index_ci_runners_on_active ON ci_runners USING btree (active, id);
...@@ -15,18 +15,18 @@ module API ...@@ -15,18 +15,18 @@ module API
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
expose :projects, with: Entities::BasicProjectDetails do |runner, options| expose :projects, with: Entities::BasicProjectDetails do |runner, options|
if options[:current_user].admin? # rubocop: disable Cop/UserAdmin if options[:current_user].admin? # rubocop: disable Cop/UserAdmin
runner.projects.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338659') runner.projects
else else
options[:current_user].authorized_projects.where(id: runner.projects).allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338659') options[:current_user].authorized_projects.where(id: runner.runner_projects.pluck(:project_id))
end end
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
expose :groups, with: Entities::BasicGroupDetails do |runner, options| expose :groups, with: Entities::BasicGroupDetails do |runner, options|
if options[:current_user].admin? # rubocop: disable Cop/UserAdmin if options[:current_user].admin? # rubocop: disable Cop/UserAdmin
runner.groups.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338659') runner.groups
else else
options[:current_user].authorized_groups.where(id: runner.groups).allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338659') options[:current_user].authorized_groups.where(id: runner.runner_namespaces.pluck(:namespace_id))
end end
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
......
...@@ -23,6 +23,26 @@ RSpec.describe Ci::Runner do ...@@ -23,6 +23,26 @@ RSpec.describe Ci::Runner do
end end
end end
describe 'projects association' do
let(:runner) { create(:ci_runner, :project) }
it 'does not create a cross-database query' do
with_cross_joins_prevented do
expect(runner.projects.count).to eq(1)
end
end
context 'when ci_runner_projects_disable_joins is disabled' do
before do
stub_feature_flags(ci_runner_projects_disable_joins: false)
end
it 'creates a cross-database query' do
expect { runner.projects.count }.to raise_error(Database::PreventCrossJoins::CrossJoinAcrossUnsupportedTablesError)
end
end
end
describe 'validation' do describe 'validation' do
it { is_expected.to validate_presence_of(:access_level) } it { is_expected.to validate_presence_of(:access_level) }
it { is_expected.to validate_presence_of(:runner_type) } it { is_expected.to validate_presence_of(:runner_type) }
...@@ -291,6 +311,30 @@ RSpec.describe Ci::Runner do ...@@ -291,6 +311,30 @@ RSpec.describe Ci::Runner do
end end
end end
describe '#only_for' do
let_it_be_with_reload(:runner) { create(:ci_runner, :project) }
let_it_be(:project) { runner.projects.first }
subject { runner.only_for?(project) }
context 'with matching project' do
it { is_expected.to be_truthy }
end
context 'without matching project' do
let_it_be(:project) { create(:project) }
it { is_expected.to be_falsey }
end
context 'with runner having multiple projects' do
let_it_be(:other_project) { create(:project) }
let_it_be(:runner_project) { create(:ci_runner_project, project: other_project, runner: runner) }
it { is_expected.to be_falsey }
end
end
describe '#assign_to' do describe '#assign_to' do
let(:project) { create(:project) } let(:project) { 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