Commit 035856f8 authored by Stan Hu's avatar Stan Hu

Fix N+1 SQL queries in PipelinesController#show

In https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56987, we lost
the N+1 query optimizations used for legacy stages that were fixed by
https://gitlab.com/gitlab-org/gitlab/-/merge_requests/21000.

To fix this issue, we use a similar approach used by that older merge
request and preload the statuses dynamically using a `StagePresenter`.
This dynamic preloading is necessary to filter out
`GenericCommitStatus`, which does not have a tags association.

Note that we the newly-introduced `with_latest_and_retried_statuses`
scope in the `Ci::Stage` model was just making extraneous queries that
weren't being used. This happend because `_stage.html.haml` previously
called `latest_statuses` and `retried_statuses` scopes, which actually
wiped out any included assocations for stages.

Relates to https://gitlab.com/gitlab-org/gitlab/-/issues/329417

Changelog: fixed
parent 4c0d2649
...@@ -227,7 +227,7 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -227,7 +227,7 @@ class Projects::PipelinesController < Projects::ApplicationController
end end
def render_show def render_show
@stages = @pipeline.stages.with_latest_and_retried_statuses @stages = @pipeline.stages
respond_to do |format| respond_to do |format|
format.html do format.html do
......
...@@ -6,6 +6,7 @@ module Ci ...@@ -6,6 +6,7 @@ module Ci
include Importable include Importable
include Ci::HasStatus include Ci::HasStatus
include Gitlab::OptimisticLocking include Gitlab::OptimisticLocking
include Presentable
enum status: Ci::HasStatus::STATUSES_ENUM enum status: Ci::HasStatus::STATUSES_ENUM
...@@ -22,12 +23,6 @@ module Ci ...@@ -22,12 +23,6 @@ module Ci
scope :ordered, -> { order(position: :asc) } scope :ordered, -> { order(position: :asc) }
scope :in_pipelines, ->(pipelines) { where(pipeline: pipelines) } scope :in_pipelines, ->(pipelines) { where(pipeline: pipelines) }
scope :by_name, ->(names) { where(name: names) } scope :by_name, ->(names) { where(name: names) }
scope :with_latest_and_retried_statuses, -> do
includes(
latest_statuses: [:pipeline, project: :namespace],
retried_statuses: [:pipeline, project: :namespace]
)
end
with_options unless: :importing? do with_options unless: :importing? do
validates :project, presence: true validates :project, presence: true
......
# frozen_string_literal: true
module Ci
class StagePresenter < Gitlab::View::Presenter::Delegated
presents :stage
def latest_ordered_statuses
preload_statuses(stage.statuses.latest_ordered)
end
def retried_ordered_statuses
preload_statuses(stage.statuses.retried_ordered)
end
private
def preload_statuses(statuses)
loaded_statuses = statuses.load
statuses.tap do |statuses|
# rubocop: disable CodeReuse/ActiveRecord
ActiveRecord::Associations::Preloader.new.preload(preloadable_statuses(loaded_statuses), %w[pipeline tags job_artifacts_archive metadata])
# rubocop: enable CodeReuse/ActiveRecord
end
end
def preloadable_statuses(statuses)
statuses.reject do |status|
status.instance_of?(::GenericCommitStatus) || status.instance_of?(::Ci::Bridge)
end
end
end
end
- stage = stage.present(current_user: current_user)
%tr %tr
%th{ colspan: 10 } %th{ colspan: 10 }
%strong %strong
...@@ -6,8 +8,8 @@ ...@@ -6,8 +8,8 @@
= ci_icon_for_status(stage.status) = ci_icon_for_status(stage.status)
&nbsp; &nbsp;
= stage.name.titleize = stage.name.titleize
= render stage.latest_statuses, stage: false, ref: false, pipeline_link: false, allow_retry: true = render stage.latest_ordered_statuses, stage: false, ref: false, pipeline_link: false, allow_retry: true
= render stage.retried_statuses, stage: false, ref: false, pipeline_link: false, retried: true = render stage.retried_ordered_statuses, stage: false, ref: false, pipeline_link: false, retried: true
%tr %tr
%td{ colspan: 10 } %td{ colspan: 10 }
&nbsp; &nbsp;
---
title: Fix N+1 SQL queries in PipelinesController#show
merge_request: 60794
author:
type: fixed
...@@ -290,6 +290,39 @@ RSpec.describe Projects::PipelinesController do ...@@ -290,6 +290,39 @@ RSpec.describe Projects::PipelinesController do
end end
end end
describe 'GET #show' do
render_views
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
subject { get_pipeline_html }
def get_pipeline_html
get :show, params: { namespace_id: project.namespace, project_id: project, id: pipeline }, format: :html
end
def create_build_with_artifacts(stage, stage_idx, name)
create(:ci_build, :artifacts, :tags, pipeline: pipeline, stage: stage, stage_idx: stage_idx, name: name)
end
before do
create_build_with_artifacts('build', 0, 'job1')
create_build_with_artifacts('build', 0, 'job2')
end
it 'avoids N+1 database queries', :request_store do
get_pipeline_html
control_count = ActiveRecord::QueryRecorder.new { get_pipeline_html }.count
expect(response).to have_gitlab_http_status(:ok)
create_build_with_artifacts('build', 0, 'job3')
expect { get_pipeline_html }.not_to exceed_query_limit(control_count)
expect(response).to have_gitlab_http_status(:ok)
end
end
describe 'GET show.json' do describe 'GET show.json' do
let(:pipeline) { create(:ci_pipeline, project: project) } let(:pipeline) { create(:ci_pipeline, project: project) }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Ci::StagePresenter do
let(:stage) { create(:ci_stage) }
let(:presenter) { described_class.new(stage) }
let!(:build) { create(:ci_build, :tags, :artifacts, pipeline: stage.pipeline, stage: stage.name) }
let!(:retried_build) { create(:ci_build, :tags, :artifacts, :retried, pipeline: stage.pipeline, stage: stage.name) }
before do
create(:generic_commit_status, pipeline: stage.pipeline, stage: stage.name)
end
shared_examples 'preloaded associations for CI status' do
it 'preloads project' do
expect(presented_stage.association(:project)).to be_loaded
end
it 'preloads build pipeline' do
expect(presented_stage.association(:pipeline)).to be_loaded
end
it 'preloads build tags' do
expect(presented_stage.association(:tags)).to be_loaded
end
it 'preloads build artifacts archive' do
expect(presented_stage.association(:job_artifacts_archive)).to be_loaded
end
it 'preloads build artifacts metadata' do
expect(presented_stage.association(:metadata)).to be_loaded
end
end
describe '#latest_ordered_statuses' do
subject(:presented_stage) { presenter.latest_ordered_statuses.second }
it_behaves_like 'preloaded associations for CI status'
end
describe '#retried_ordered_statuses' do
subject(:presented_stage) { presenter.retried_ordered_statuses.first }
it_behaves_like 'preloaded associations for CI status'
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