Improve performance of finding last deployed environment
This commit is contained in:
		
							parent
							
								
									3aa1264dc6
								
							
						
					
					
						commit
						c8b63a28af
					
				|  | @ -31,7 +31,7 @@ class Projects::BlobController < Projects::ApplicationController | |||
| 
 | ||||
|   def show | ||||
|     branch_name = @ref if @repository.branch_exists?(@ref) | ||||
|     @environment = @project.latest_environment_for(@commit, ref: branch_name) | ||||
|     @environment = @project.environments_for(commit: @commit, ref: branch_name).last | ||||
|     @environment = nil unless can?(current_user, :read_environment, @environment) | ||||
|   end | ||||
| 
 | ||||
|  |  | |||
|  | @ -96,7 +96,7 @@ class Projects::CommitController < Projects::ApplicationController | |||
|     @diffs = commit.diffs(opts) | ||||
|     @notes_count = commit.notes.count | ||||
| 
 | ||||
|     @environment = @project.latest_environment_for(@commit) | ||||
|     @environment = @project.environments_for(commit: @commit).last | ||||
|     @environment = nil unless can?(current_user, :read_environment, @environment) | ||||
|   end | ||||
| 
 | ||||
|  |  | |||
|  | @ -58,7 +58,7 @@ class Projects::CompareController < Projects::ApplicationController | |||
|       @diffs = @compare.diffs(diff_options) | ||||
| 
 | ||||
|       branch_name = @head_ref if @repository.branch_exists?(@head_ref) | ||||
|       @environment = @project.latest_environment_for(@commit, ref: branch_name) | ||||
|       @environment = @project.environments_for(commit: @commit, ref: branch_name).last | ||||
|       @environment = nil unless can?(current_user, :read_environment, @environment) | ||||
| 
 | ||||
|       @diff_notes_disabled = true | ||||
|  |  | |||
|  | @ -10,7 +10,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController | |||
| 
 | ||||
|   def index | ||||
|     @scope = params[:scope] | ||||
|     @environments = project.environments | ||||
|     @environments = project.environments.includes(:last_deployment) | ||||
| 
 | ||||
|     respond_to do |format| | ||||
|       format.html | ||||
|  |  | |||
|  | @ -103,7 +103,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController | |||
|       end | ||||
|     end | ||||
| 
 | ||||
|     @environment = @merge_request.latest_environment | ||||
|     @environment = @merge_request.environments.last | ||||
|     @environment = nil unless can?(current_user, :read_environment, @environment) | ||||
| 
 | ||||
|     respond_to do |format| | ||||
|  | @ -248,7 +248,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController | |||
|                  end | ||||
|         @diff_notes_disabled = true | ||||
| 
 | ||||
|         @environment = @merge_request.latest_environment | ||||
|         @environment = @merge_request.environments.last | ||||
|         @environment = nil unless can?(current_user, :read_environment, @environment) | ||||
| 
 | ||||
|         render json: { html: view_to_html_string('projects/merge_requests/_new_diffs', diffs: @diffs, environment: @environment) } | ||||
|  |  | |||
|  | @ -9,6 +9,7 @@ module Ci | |||
|     belongs_to :erased_by, class_name: 'User' | ||||
| 
 | ||||
|     has_many :deployments, as: :deployable | ||||
|     has_one :last_deployment, -> { order('deployments.id DESC') }, as: :deployable, class_name: 'Deployment' | ||||
| 
 | ||||
|     # The "environment" field for builds is a String, and is the unexpanded name | ||||
|     def persisted_environment | ||||
|  | @ -183,10 +184,6 @@ module Ci | |||
|       success? && !last_deployment.try(:last?) | ||||
|     end | ||||
| 
 | ||||
|     def last_deployment | ||||
|       deployments.last | ||||
|     end | ||||
| 
 | ||||
|     def depends_on_builds | ||||
|       # Get builds of the same type | ||||
|       latest_builds = self.pipeline.builds.latest | ||||
|  |  | |||
|  | @ -6,7 +6,8 @@ class Environment < ActiveRecord::Base | |||
| 
 | ||||
|   belongs_to :project, required: true, validate: true | ||||
| 
 | ||||
|   has_many :deployments | ||||
|   has_many :deployments, dependent: :destroy | ||||
|   has_one :last_deployment, -> { order('deployments.id DESC') }, class_name: 'Deployment' | ||||
| 
 | ||||
|   before_validation :nullify_external_url | ||||
|   before_validation :generate_slug, if: ->(env) { env.slug.blank? } | ||||
|  | @ -37,6 +38,7 @@ class Environment < ActiveRecord::Base | |||
| 
 | ||||
|   scope :available, -> { with_state(:available) } | ||||
|   scope :stopped, -> { with_state(:stopped) } | ||||
|   scope :order_by_last_deployed_at, -> { order(Gitlab::Database.nulls_first_order('(SELECT MAX(id) FROM deployments WHERE deployments.environment_id = environments.id)', 'ASC')) } | ||||
| 
 | ||||
|   state_machine :state, initial: :available do | ||||
|     event :start do | ||||
|  | @ -51,14 +53,6 @@ class Environment < ActiveRecord::Base | |||
|     state :stopped | ||||
|   end | ||||
| 
 | ||||
|   def self.latest_for_commit(environments, commit) | ||||
|     environments.sort_by do |environment| | ||||
|       deployment = environment.first_deployment_for(commit) | ||||
| 
 | ||||
|       deployment.try(:created_at) || DateTime.parse('1970-01-01') | ||||
|     end.last | ||||
|   end | ||||
| 
 | ||||
|   def predefined_variables | ||||
|     [ | ||||
|       { key: 'CI_ENVIRONMENT_NAME', value: name, public: true }, | ||||
|  | @ -70,10 +64,6 @@ class Environment < ActiveRecord::Base | |||
|     ref.to_s == last_deployment.try(:ref) | ||||
|   end | ||||
| 
 | ||||
|   def last_deployment | ||||
|     deployments.last | ||||
|   end | ||||
| 
 | ||||
|   def nullify_external_url | ||||
|     self.external_url = nil if self.external_url.blank? | ||||
|   end | ||||
|  | @ -95,6 +85,10 @@ class Environment < ActiveRecord::Base | |||
|     last_deployment.includes_commit?(commit) | ||||
|   end | ||||
| 
 | ||||
|   def last_deployed_at | ||||
|     last_deployment.try(:created_at) | ||||
|   end | ||||
| 
 | ||||
|   def update_merge_request_metrics? | ||||
|     (environment_type || name) == "production" | ||||
|   end | ||||
|  |  | |||
|  | @ -720,21 +720,15 @@ class MergeRequest < ActiveRecord::Base | |||
| 
 | ||||
|     @environments ||= begin | ||||
|       target_envs = target_project.environments_for( | ||||
|         target_branch, commit: diff_head_commit, with_tags: true) | ||||
|         ref: target_branch, commit: diff_head_commit, with_tags: true) | ||||
| 
 | ||||
|       source_envs = source_project.environments_for( | ||||
|         source_branch, commit: diff_head_commit) if source_project | ||||
|         ref: source_branch, commit: diff_head_commit) if source_project | ||||
| 
 | ||||
|       (target_envs.to_a + source_envs.to_a).uniq | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def latest_environment | ||||
|     return @latest_environment if defined?(@latest_environment) | ||||
| 
 | ||||
|     @latest_environment = Environment.latest_for_commit(environments, diff_head_commit) | ||||
|   end | ||||
| 
 | ||||
|   def state_human_name | ||||
|     if merged? | ||||
|       "Merged" | ||||
|  |  | |||
|  | @ -1306,7 +1306,7 @@ class Project < ActiveRecord::Base | |||
|     Gitlab::Redis.with { |redis| redis.del(pushes_since_gc_redis_key) } | ||||
|   end | ||||
| 
 | ||||
|   def environments_for(ref, commit: nil, with_tags: false) | ||||
|   def environments_for(ref: nil, commit: nil, with_tags: false) | ||||
|     deps = | ||||
|       if ref | ||||
|         deployments_query = with_tags ? 'ref = ? OR tag IS TRUE' : 'ref = ?' | ||||
|  | @ -1322,22 +1322,17 @@ class Project < ActiveRecord::Base | |||
|       .select(:environment_id) | ||||
| 
 | ||||
|     environments_found = environments.available | ||||
|       .where(id: environment_ids).to_a | ||||
|       .where(id: environment_ids).order_by_last_deployed_at.to_a | ||||
| 
 | ||||
|     return environments_found unless commit | ||||
|     return environments_found unless ref && commit | ||||
| 
 | ||||
|     environments_found.select do |environment| | ||||
|       environment.includes_commit?(commit) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def latest_environment_for(commit, ref: nil) | ||||
|     environments = environments_for(ref, commit: commit) | ||||
|     Environment.latest_for_commit(environments, commit) | ||||
|   end | ||||
| 
 | ||||
|   def environments_recently_updated_on_branch(branch) | ||||
|     environments_for(branch).select do |environment| | ||||
|     environments_for(ref: branch).select do |environment| | ||||
|       environment.recently_updated_on_branch?(branch) | ||||
|     end | ||||
|   end | ||||
|  |  | |||
|  | @ -1190,6 +1190,7 @@ class Repository | |||
|   def route_map_for(sha) | ||||
|     blob = blob_at(sha, ROUTE_MAP_PATH) | ||||
|     return unless blob | ||||
|      | ||||
|     blob.load_all_data!(self) | ||||
|     blob.data | ||||
|   end | ||||
|  | @ -1197,6 +1198,7 @@ class Repository | |||
|   def gitlab_ci_yml_for(sha) | ||||
|     blob = blob_at(sha, GITLAB_CI_YML_PATH) | ||||
|     return unless blob | ||||
| 
 | ||||
|     blob.load_all_data!(self) | ||||
|     blob.data | ||||
|   end | ||||
|  |  | |||
|  | @ -35,6 +35,20 @@ module Gitlab | |||
|       order | ||||
|     end | ||||
| 
 | ||||
|     def self.nulls_first_order(field, direction = 'ASC') | ||||
|       order = "#{field} #{direction}" | ||||
| 
 | ||||
|       if Gitlab::Database.postgresql? | ||||
|         order << ' NULLS FIRST' | ||||
|       else | ||||
|         # `field IS NULL` will be `0` for non-NULL columns and `1` for NULL | ||||
|         # columns. In the (default) ascending order, `0` comes first. | ||||
|         order.prepend("#{field} IS NULL, ") if direction == 'DESC' | ||||
|       end | ||||
| 
 | ||||
|       order | ||||
|     end | ||||
| 
 | ||||
|     def self.random | ||||
|       Gitlab::Database.postgresql? ? "RANDOM()" : "RAND()" | ||||
|     end | ||||
|  |  | |||
|  | @ -55,6 +55,22 @@ describe Gitlab::Database, lib: true do | |||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '.nulls_first_order' do | ||||
|     context 'when using PostgreSQL' do | ||||
|       before { expect(described_class).to receive(:postgresql?).and_return(true) } | ||||
| 
 | ||||
|       it { expect(described_class.nulls_first_order('column', 'ASC')).to eq 'column ASC NULLS FIRST'} | ||||
|       it { expect(described_class.nulls_first_order('column', 'DESC')).to eq 'column DESC NULLS FIRST'} | ||||
|     end | ||||
| 
 | ||||
|     context 'when using MySQL' do | ||||
|       before { expect(described_class).to receive(:postgresql?).and_return(false) } | ||||
| 
 | ||||
|       it { expect(described_class.nulls_first_order('column', 'ASC')).to eq 'column ASC'} | ||||
|       it { expect(described_class.nulls_first_order('column', 'DESC')).to eq 'column IS NULL, column DESC'} | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#true_value' do | ||||
|     it 'returns correct value for PostgreSQL' do | ||||
|       expect(described_class).to receive(:postgresql?).and_return(true) | ||||
|  |  | |||
|  | @ -22,22 +22,17 @@ describe Environment, models: true do | |||
|   it { is_expected.to validate_length_of(:external_url).is_at_most(255) } | ||||
|   it { is_expected.to validate_uniqueness_of(:external_url).scoped_to(:project_id) } | ||||
| 
 | ||||
|   describe '.latest_for_commit' do | ||||
|   describe '.order_by_last_deployed_at' do | ||||
|     let(:project) { create(:project) } | ||||
|     let!(:environment1) { create(:environment, project: project) } | ||||
|     let!(:environment2) { create(:environment, project: project) } | ||||
|     let!(:environment3) { create(:environment, project: project) } | ||||
|     let!(:deployment1) { create(:deployment, environment: environment1) } | ||||
|     let!(:deployment2) { create(:deployment, environment: environment2) } | ||||
|     let(:commit) { RepoHelpers.sample_commit } | ||||
|     let!(:deployment1) { create(:deployment, environment: environment1) } | ||||
| 
 | ||||
|     before do | ||||
|       allow(environment1).to receive(:first_deployment_for).with(commit).and_return(deployment1) | ||||
|       allow(environment2).to receive(:first_deployment_for).with(commit).and_return(deployment2) | ||||
|       allow(environment3).to receive(:first_deployment_for).with(commit).and_return(nil) | ||||
|     end | ||||
| 
 | ||||
|     it 'returns the environment that the commit was last deployed to' do | ||||
|       expect(Environment.latest_for_commit([environment1, environment2, environment3], commit)).to be(environment2) | ||||
|     it 'returns the environments in order of having been last deployed' do | ||||
|       # byebug | ||||
|       expect(project.environments.order_by_last_deployed_at.to_a).to eq([environment3, environment2, environment1]) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|  |  | |||
|  | @ -1069,30 +1069,6 @@ describe MergeRequest, models: true do | |||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#latest_environment' do | ||||
|     let(:project) { subject.project } | ||||
|     let!(:environment1) { create(:environment, project: project) } | ||||
|     let!(:environment2) { create(:environment, project: project) } | ||||
|     let!(:environment3) { create(:environment, project: project) } | ||||
|     let!(:deployment1) { create(:deployment, environment: environment1, ref: 'master', sha: commit.id) } | ||||
|     let!(:deployment2) { create(:deployment, environment: environment2, ref: 'feature', sha: commit.id) } | ||||
|     let(:commit) { subject.diff_head_commit } | ||||
| 
 | ||||
|     before do | ||||
|       allow(environment1).to receive(:first_deployment_for).with(commit).and_return(deployment1) | ||||
|       allow(environment2).to receive(:first_deployment_for).with(commit).and_return(deployment2) | ||||
|       allow(environment3).to receive(:first_deployment_for).with(commit).and_return(nil) | ||||
|     end | ||||
| 
 | ||||
|     before do | ||||
|       allow(subject).to receive(:environments).and_return([environment1, environment2, environment3]) | ||||
|     end | ||||
| 
 | ||||
|     it 'returns the environment that the commit was last deployed to' do | ||||
|       expect(subject.latest_environment).to eq(environment2) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe "#reload_diff" do | ||||
|     let(:note) { create(:diff_note_on_merge_request, project: subject.project, noteable: subject) } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1726,17 +1726,17 @@ describe Project, models: true do | |||
|       end | ||||
| 
 | ||||
|       it 'returns environment when with_tags is set' do | ||||
|         expect(project.environments_for('master', commit: project.commit, with_tags: true)) | ||||
|         expect(project.environments_for(ref: 'master', commit: project.commit, with_tags: true)) | ||||
|           .to contain_exactly(environment) | ||||
|       end | ||||
| 
 | ||||
|       it 'does not return environment when no with_tags is set' do | ||||
|         expect(project.environments_for('master', commit: project.commit)) | ||||
|         expect(project.environments_for(ref: 'master', commit: project.commit)) | ||||
|           .to be_empty | ||||
|       end | ||||
| 
 | ||||
|       it 'does not return environment when commit is not part of deployment' do | ||||
|         expect(project.environments_for('master', commit: project.commit('feature'))) | ||||
|         expect(project.environments_for(ref: 'master', commit: project.commit('feature'))) | ||||
|           .to be_empty | ||||
|       end | ||||
|     end | ||||
|  | @ -1747,22 +1747,22 @@ describe Project, models: true do | |||
|       end | ||||
| 
 | ||||
|       it 'returns environment when ref is set' do | ||||
|         expect(project.environments_for('master', commit: project.commit)) | ||||
|         expect(project.environments_for(ref: 'master', commit: project.commit)) | ||||
|           .to contain_exactly(environment) | ||||
|       end | ||||
| 
 | ||||
|       it 'does not environment when ref is different' do | ||||
|         expect(project.environments_for('feature', commit: project.commit)) | ||||
|         expect(project.environments_for(ref: 'feature', commit: project.commit)) | ||||
|           .to be_empty | ||||
|       end | ||||
| 
 | ||||
|       it 'does not return environment when commit is not part of deployment' do | ||||
|         expect(project.environments_for('master', commit: project.commit('feature'))) | ||||
|         expect(project.environments_for(ref: 'master', commit: project.commit('feature'))) | ||||
|           .to be_empty | ||||
|       end | ||||
| 
 | ||||
|       it 'returns environment when commit constraint is not set' do | ||||
|         expect(project.environments_for('master')) | ||||
|         expect(project.environments_for(ref: 'master')) | ||||
|           .to contain_exactly(environment) | ||||
|       end | ||||
|     end | ||||
|  | @ -1773,48 +1773,12 @@ describe Project, models: true do | |||
|       end | ||||
| 
 | ||||
|       it 'returns environment' do | ||||
|         expect(project.environments_for(nil, commit: project.commit)) | ||||
|         expect(project.environments_for(commit: project.commit)) | ||||
|           .to contain_exactly(environment) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#latest_environment_for' do | ||||
|     let(:project) { create(:project) } | ||||
|     let!(:environment1) { create(:environment, project: project) } | ||||
|     let!(:environment2) { create(:environment, project: project) } | ||||
|     let!(:environment3) { create(:environment, project: project) } | ||||
|     let!(:deployment1) { create(:deployment, environment: environment1, ref: 'master', sha: commit.id) } | ||||
|     let!(:deployment2) { create(:deployment, environment: environment2, ref: 'feature', sha: commit.id) } | ||||
|     let(:commit) { project.commit } | ||||
| 
 | ||||
|     before do | ||||
|       allow(environment1).to receive(:first_deployment_for).with(commit).and_return(deployment1) | ||||
|       allow(environment2).to receive(:first_deployment_for).with(commit).and_return(deployment2) | ||||
|       allow(environment3).to receive(:first_deployment_for).with(commit).and_return(nil) | ||||
|     end | ||||
| 
 | ||||
|     context 'when specifying a ref' do | ||||
|       before do | ||||
|         allow(project).to receive(:environments_for).with('master', commit: commit).and_return([environment1]) | ||||
|       end | ||||
| 
 | ||||
|       it 'returns the environment that the commit was last deployed to from that ref' do | ||||
|         expect(project.latest_environment_for(commit, ref: 'master')).to eq(environment1) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when not specifying a ref' do | ||||
|       before do | ||||
|         allow(project).to receive(:environments_for).with(nil, commit: commit).and_return([environment1, environment2]) | ||||
|       end | ||||
| 
 | ||||
|       it 'returns the environment that the commit was last deployed to' do | ||||
|         expect(project.latest_environment_for(commit)).to eq(environment2) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#environments_recently_updated_on_branch' do | ||||
|     let(:project) { create(:project, :repository) } | ||||
|     let(:environment) { create(:environment, project: project) } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue