Commit cbcfc1f5 authored by Mark Chao's avatar Mark Chao

Merge branch 'optimize-environments-serializer' into 'master'

Optimize environment serializer by preloading associated entities [RUN ALL RSPEC] [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!58748
parents 6e41432d f82d82fd
...@@ -377,11 +377,11 @@ module Ci ...@@ -377,11 +377,11 @@ module Ci
end end
def other_manual_actions def other_manual_actions
pipeline.manual_actions.where.not(name: name) pipeline.manual_actions.reject { |action| action.name == self.name }
end end
def other_scheduled_actions def other_scheduled_actions
pipeline.scheduled_actions.where.not(name: name) pipeline.scheduled_actions.reject { |action| action.name == self.name }
end end
def pages_generator? def pages_generator?
......
...@@ -171,7 +171,7 @@ class Deployment < ApplicationRecord ...@@ -171,7 +171,7 @@ class Deployment < ApplicationRecord
end end
def commit def commit
project.commit(sha) @commit ||= project.commit(sha)
end end
def commit_title def commit_title
...@@ -250,7 +250,7 @@ class Deployment < ApplicationRecord ...@@ -250,7 +250,7 @@ class Deployment < ApplicationRecord
return unless on_stop.present? return unless on_stop.present?
return unless manual_actions return unless manual_actions
@stop_action ||= manual_actions.find_by(name: on_stop) @stop_action ||= manual_actions.find { |action| action.name == self.on_stop }
end end
def finished_at def finished_at
......
...@@ -24,13 +24,13 @@ class Environment < ApplicationRecord ...@@ -24,13 +24,13 @@ class Environment < ApplicationRecord
has_many :self_managed_prometheus_alert_events, inverse_of: :environment has_many :self_managed_prometheus_alert_events, inverse_of: :environment
has_many :alert_management_alerts, class_name: 'AlertManagement::Alert', inverse_of: :environment has_many :alert_management_alerts, class_name: 'AlertManagement::Alert', inverse_of: :environment
has_one :last_deployment, -> { success.order('deployments.id DESC') }, class_name: 'Deployment' has_one :last_deployment, -> { success.order('deployments.id DESC') }, class_name: 'Deployment', inverse_of: :environment
has_one :last_deployable, through: :last_deployment, source: 'deployable', source_type: 'CommitStatus' has_one :last_deployable, through: :last_deployment, source: 'deployable', source_type: 'CommitStatus'
has_one :last_pipeline, through: :last_deployable, source: 'pipeline' has_one :last_pipeline, through: :last_deployable, source: 'pipeline'
has_one :last_visible_deployment, -> { visible.distinct_on_environment }, inverse_of: :environment, class_name: 'Deployment' has_one :last_visible_deployment, -> { visible.distinct_on_environment }, inverse_of: :environment, class_name: 'Deployment'
has_one :last_visible_deployable, through: :last_visible_deployment, source: 'deployable', source_type: 'CommitStatus' has_one :last_visible_deployable, through: :last_visible_deployment, source: 'deployable', source_type: 'CommitStatus'
has_one :last_visible_pipeline, through: :last_visible_deployable, source: 'pipeline' has_one :last_visible_pipeline, through: :last_visible_deployable, source: 'pipeline'
has_one :upcoming_deployment, -> { running.order('deployments.id DESC') }, class_name: 'Deployment' has_one :upcoming_deployment, -> { running.order('deployments.id DESC') }, class_name: 'Deployment', inverse_of: :environment
has_one :latest_opened_most_severe_alert, -> { order_severity_with_open_prometheus_alert }, class_name: 'AlertManagement::Alert', inverse_of: :environment has_one :latest_opened_most_severe_alert, -> { order_severity_with_open_prometheus_alert }, class_name: 'AlertManagement::Alert', inverse_of: :environment
before_validation :nullify_external_url before_validation :nullify_external_url
......
...@@ -23,7 +23,7 @@ class EnvironmentSerializer < BaseSerializer ...@@ -23,7 +23,7 @@ class EnvironmentSerializer < BaseSerializer
latest: super(item.latest, opts) } latest: super(item.latest, opts) }
end end
else else
super(resource, opts) super(batch_load(resource), opts)
end end
end end
...@@ -41,11 +41,59 @@ class EnvironmentSerializer < BaseSerializer ...@@ -41,11 +41,59 @@ class EnvironmentSerializer < BaseSerializer
# immediately. # immediately.
items = @paginator.paginate(items) if paginated? items = @paginator.paginate(items) if paginated?
environments = resource.where(id: items.map(&:last_id)).index_by(&:id) environments = batch_load(resource.where(id: items.map(&:last_id)))
environments_by_id = environments.index_by(&:id)
items.map do |item| items.map do |item|
Item.new(item.folder, item.size, environments[item.last_id]) Item.new(item.folder, item.size, environments_by_id[item.last_id])
end end
end end
def batch_load(resource)
resource = resource.preload(environment_associations)
resource.all.tap do |environments|
environments.each do |environment|
# Batch loading the commits of the deployments
environment.last_deployment&.commit&.try(:lazy_author)
environment.upcoming_deployment&.commit&.try(:lazy_author)
end
end
end
def environment_associations
{
last_deployment: deployment_associations,
upcoming_deployment: deployment_associations,
project: project_associations
}
end
def deployment_associations
{
user: [],
cluster: [],
project: [],
deployable: {
user: [],
metadata: [],
pipeline: {
manual_actions: [],
scheduled_actions: []
},
project: project_associations
}
}
end
def project_associations
{
project_feature: [],
route: [],
namespace: :route
}
end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
end end
EnvironmentSerializer.prepend_if_ee('EE::EnvironmentSerializer')
---
title: Optimize environment serializer to reduce N+1 problems
merge_request: 58748
author:
type: performance
...@@ -672,7 +672,7 @@ module EE ...@@ -672,7 +672,7 @@ module EE
key = "protected_environment_by_name:#{id}:#{environment_name}" key = "protected_environment_by_name:#{id}:#{environment_name}"
::Gitlab::SafeRequestStore.fetch(key) do ::Gitlab::SafeRequestStore.fetch(key) do
protected_environments.find_by(name: environment_name) protected_environments.find { |pe| pe.name == environment_name }
end end
end end
......
# frozen_string_literal: true
module EE
module EnvironmentSerializer
extend ActiveSupport::Concern
extend ::Gitlab::Utils::Override
override :environment_associations
def environment_associations
super.deep_merge(latest_opened_most_severe_alert: [])
end
override :project_associations
def project_associations
super.deep_merge(protected_environments: [])
end
end
end
...@@ -1840,7 +1840,7 @@ RSpec.describe Project do ...@@ -1840,7 +1840,7 @@ RSpec.describe Project do
end end
describe '#protected_environment_by_name' do describe '#protected_environment_by_name' do
let_it_be(:project) { create(:project) } let_it_be(:project, reload: true) { create(:project) }
subject { project.protected_environment_by_name('production') } subject { project.protected_environment_by_name('production') }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe EE::EnvironmentSerializer do
let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) { create(:project, :repository) }
before_all do
project.add_developer(user)
end
before do
stub_licensed_features(environment_alerts: true)
end
it_behaves_like 'avoid N+1 on environments serialization'
def create_environment_with_associations(project)
create(:environment, project: project).tap do |environment|
create(:deployment, :success, environment: environment, project: project)
create(:deployment, :running, environment: environment, project: project)
create(:protected_environment, :maintainers_can_deploy, name: environment.name, project: project)
prometheus_alert = create(:prometheus_alert, project: project, environment: environment)
create(:alert_management_alert, :triggered, :prometheus, project: project, environment: environment, prometheus_alert: prometheus_alert)
end
end
end
...@@ -3,8 +3,10 @@ ...@@ -3,8 +3,10 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe EnvironmentSerializer do RSpec.describe EnvironmentSerializer do
let(:user) { create(:user) } include CreateEnvironmentsHelpers
let(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be(:project, reload: true) { create(:project, :repository) }
let(:json) do let(:json) do
described_class described_class
...@@ -12,43 +14,18 @@ RSpec.describe EnvironmentSerializer do ...@@ -12,43 +14,18 @@ RSpec.describe EnvironmentSerializer do
.represent(resource) .represent(resource)
end end
before do before_all do
project.add_developer(user) project.add_developer(user)
end end
context 'when there is a single object provided' do it_behaves_like 'avoid N+1 on environments serialization'
let(:project) { create(:project, :repository) }
let(:deployable) { create(:ci_build) }
let(:deployment) do
create(:deployment, :success,
deployable: deployable,
user: user,
project: project,
sha: project.commit.id)
end
let(:resource) { deployment.environment }
before do
create(:ci_build, :manual, name: 'manual1', pipeline: deployable.pipeline)
end
it 'contains important elements of environment' do
expect(json)
.to include(:name, :external_url, :environment_path, :last_deployment)
end
it 'contains relevant information about last deployment' do context 'when there is a collection of objects provided' do
last_deployment = json.fetch(:last_deployment) let(:resource) { project.environments }
expect(last_deployment) before_all do
.to include(:ref, :user, :commit, :deployable, :manual_actions) create_list(:environment, 2, project: project)
end end
end
context 'when there is a collection of objects provided' do
let(:project) { create(:project) }
let(:resource) { create_list(:environment, 2) }
it 'contains important elements of environment' do it 'contains important elements of environment' do
expect(json.first) expect(json.first)
...@@ -207,4 +184,11 @@ RSpec.describe EnvironmentSerializer do ...@@ -207,4 +184,11 @@ RSpec.describe EnvironmentSerializer do
end end
end end
end end
def create_environment_with_associations(project)
create(:environment, project: project).tap do |environment|
create(:deployment, :success, environment: environment, project: project)
create(:deployment, :running, environment: environment, project: project)
end
end
end end
# frozen_string_literal: true
RSpec.shared_examples 'avoid N+1 on environments serialization' do
it 'avoids N+1 database queries with grouping', :request_store do
create_environment_with_associations(project)
control = ActiveRecord::QueryRecorder.new { serialize(grouping: true) }
create_environment_with_associations(project)
expect { serialize(grouping: true) }.not_to exceed_query_limit(control.count)
end
it 'avoids N+1 database queries without grouping', :request_store do
create_environment_with_associations(project)
control = ActiveRecord::QueryRecorder.new { serialize(grouping: false) }
create_environment_with_associations(project)
expect { serialize(grouping: false) }.not_to exceed_query_limit(control.count)
end
def serialize(grouping:)
EnvironmentSerializer.new(current_user: user, project: project).yield_self do |serializer|
serializer.within_folders if grouping
serializer.represent(Environment.where(project: project))
end
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